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,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ # Streaming result iterator for memory-efficient parsing.
5
+ #
6
+ # Provides an Enumerable interface over parse results, allowing
7
+ # incremental processing without materializing the entire tree.
8
+ # Uses depth-first traversal to minimize memory usage.
9
+ #
10
+ # == Motivation
11
+ #
12
+ # Traditional parsing materializes the entire parse tree in memory:
13
+ #
14
+ # results = parser.parse(large_input) # Full tree in memory
15
+ # results.each { |node| process(node) }
16
+ #
17
+ # For large inputs, this can consume significant memory. ResultStream
18
+ # provides lazy iteration without full tree materialization:
19
+ #
20
+ # stream = ResultStream.new(parser.parse(input))
21
+ # stream.each { |node| process(node) } # Processes incrementally
22
+ #
23
+ # == Usage
24
+ #
25
+ # Basic iteration:
26
+ #
27
+ # stream = ResultStream.new(parse_tree)
28
+ # stream.each { |node| puts node }
29
+ #
30
+ # Filtering (leverages Enumerable):
31
+ #
32
+ # stream.select { |node| node.is_a?(Hash) }.each { |hash| process(hash) }
33
+ #
34
+ # Mapping:
35
+ #
36
+ # transformed = stream.map { |node| transform(node) }
37
+ #
38
+ # == Performance Characteristics
39
+ #
40
+ # - Memory: O(tree depth) instead of O(tree size)
41
+ # - Speed: Minimal overhead (~1-2% vs direct iteration)
42
+ # - Lazy evaluation: Nodes processed on-demand
43
+ #
44
+ # == Integration with Parser
45
+ #
46
+ # Can be used directly with parse results:
47
+ #
48
+ # parser = MyParser.new
49
+ # result = parser.parse(input)
50
+ # stream = ResultStream.new(result)
51
+ #
52
+ # Or through the optional stream method on Base:
53
+ #
54
+ # stream = parser.stream(input) # If available
55
+ #
56
+ class ResultStream
57
+ include Enumerable
58
+
59
+ # Creates a new result stream.
60
+ #
61
+ # @param tree [Object] Parse tree (Hash, Array, or scalar)
62
+ def initialize(tree)
63
+ @tree = tree
64
+ end
65
+
66
+ # Iterates over all nodes in the parse tree.
67
+ # Uses depth-first traversal to minimize memory usage.
68
+ #
69
+ # Traversal order:
70
+ # 1. Current node (pre-order)
71
+ # 2. Child nodes (recursive)
72
+ #
73
+ # This ensures that:
74
+ # - Only the current path is kept in memory (stack)
75
+ # - Parent nodes are yielded before children
76
+ # - Natural processing order for most use cases
77
+ #
78
+ # @yield [node] Each node in the tree
79
+ # @yieldparam node [Object] Current node (Hash, Array, or scalar)
80
+ # @return [Enumerator] if no block given
81
+ #
82
+ # @example Basic iteration
83
+ # stream.each { |node| puts node.class }
84
+ #
85
+ # @example Lazy enumeration
86
+ # enum = stream.each # Returns Enumerator
87
+ # enum.next # Get next node
88
+ #
89
+ def each(&)
90
+ return enum_for(:each) unless block_given?
91
+
92
+ traverse(@tree, &)
93
+ self
94
+ end
95
+
96
+ # Filters nodes by type.
97
+ #
98
+ # @param klass [Class] Class to filter by
99
+ # @return [Enumerator] Filtered nodes
100
+ #
101
+ # @example Get all hash nodes
102
+ # stream.nodes_of_type(Hash)
103
+ #
104
+ def nodes_of_type(klass)
105
+ grep(klass)
106
+ end
107
+
108
+ # Returns all hash nodes in the tree.
109
+ #
110
+ # @return [Enumerator] Hash nodes
111
+ #
112
+ # @example
113
+ # stream.hashes.each { |h| puts h.keys }
114
+ #
115
+ def hashes
116
+ nodes_of_type(Hash)
117
+ end
118
+
119
+ # Returns all array nodes in the tree.
120
+ #
121
+ # @return [Enumerator] Array nodes
122
+ #
123
+ # @example
124
+ # stream.arrays.each { |a| puts a.size }
125
+ #
126
+ def arrays
127
+ nodes_of_type(Array)
128
+ end
129
+
130
+ # Returns all scalar nodes (non-Hash, non-Array).
131
+ #
132
+ # @return [Enumerator] Scalar nodes
133
+ #
134
+ # @example
135
+ # stream.scalars.each { |s| puts s }
136
+ #
137
+ def scalars
138
+ select { |node| !node.is_a?(Hash) && !node.is_a?(Array) }
139
+ end
140
+
141
+ # Returns nodes matching a predicate at a specific depth.
142
+ #
143
+ # @param depth [Integer] Tree depth (0 = root)
144
+ # @yield [node] Predicate to test each node
145
+ # @return [Enumerator] Matching nodes
146
+ #
147
+ # @example Get all nodes at depth 2
148
+ # stream.at_depth(2) { true }
149
+ #
150
+ def at_depth(target_depth, &predicate)
151
+ predicate ||= proc { true }
152
+ depth_traverse(@tree, 0, target_depth, &predicate)
153
+ end
154
+
155
+ # Counts total nodes in the tree.
156
+ #
157
+ # @return [Integer] Total node count
158
+ #
159
+ # @example
160
+ # stream.count # => 42
161
+ #
162
+ def count
163
+ counter = 0
164
+ each { counter += 1 }
165
+ counter
166
+ end
167
+
168
+ # Returns maximum depth of the tree.
169
+ #
170
+ # @return [Integer] Maximum depth
171
+ #
172
+ # @example
173
+ # stream.max_depth # => 5
174
+ #
175
+ def max_depth
176
+ find_max_depth(@tree, 0)
177
+ end
178
+
179
+ private
180
+
181
+ # Depth-first tree traversal with pre-order visiting.
182
+ #
183
+ # @param node [Object] Current node
184
+ # @yield [node] Each visited node
185
+ #
186
+ def traverse(node, &block)
187
+ # Yield current node first (pre-order)
188
+ yield node
189
+
190
+ # Recursively traverse children
191
+ case node
192
+ when Array
193
+ node.each { |item| traverse(item, &block) }
194
+ when Hash
195
+ node.each_value { |value| traverse(value, &block) }
196
+ end
197
+ # Scalars have no children, stop here
198
+ end
199
+
200
+ # Depth-aware traversal for filtering by level.
201
+ #
202
+ # @param node [Object] Current node
203
+ # @param current_depth [Integer] Current depth in tree
204
+ # @param target_depth [Integer] Depth to match
205
+ # @yield [node] Matching nodes at target depth
206
+ # @return [Enumerator]
207
+ #
208
+ def depth_traverse(node, current_depth, target_depth, &block)
209
+ unless block
210
+ return enum_for(:depth_traverse, node, current_depth,
211
+ target_depth)
212
+ end
213
+
214
+ # Check if we're at target depth
215
+ return [node].to_enum if current_depth == target_depth && yield(node)
216
+
217
+ # Recurse to children if not at target depth yet
218
+ results = []
219
+ if current_depth < target_depth
220
+ case node
221
+ when Array
222
+ node.each do |item|
223
+ depth_traverse(item, current_depth + 1, target_depth,
224
+ &block).each do |result|
225
+ results << result
226
+ end
227
+ end
228
+ when Hash
229
+ node.each_value do |value|
230
+ depth_traverse(value, current_depth + 1, target_depth,
231
+ &block).each do |result|
232
+ results << result
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ results.to_enum
239
+ end
240
+
241
+ # Find maximum depth of tree recursively.
242
+ #
243
+ # @param node [Object] Current node
244
+ # @param current_depth [Integer] Current depth
245
+ # @return [Integer] Maximum depth from this node
246
+ #
247
+ def find_max_depth(node, current_depth)
248
+ max = current_depth
249
+
250
+ case node
251
+ when Array
252
+ node.each do |item|
253
+ depth = find_max_depth(item, current_depth + 1)
254
+ max = depth if depth > max
255
+ end
256
+ when Hash
257
+ node.each_value do |value|
258
+ depth = find_max_depth(value, current_depth + 1)
259
+ max = depth if depth > max
260
+ end
261
+ end
262
+
263
+ max
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher for parsing expectations. Provides a fluent DSL for
4
+ # specifying parsing behavior in tests.
5
+ #
6
+ # @example Basic usage
7
+ # expect(parser).to parse("input")
8
+ #
9
+ # @example With expected output
10
+ # expect(parser).to parse("123").as(123)
11
+ #
12
+ # @example With block validation
13
+ # expect(parser).to parse("input").as { |result| result.size > 0 }
14
+ #
15
+ # Inspired by RSpec matcher patterns and Parslet's testing utilities.
16
+ #
17
+ RSpec::Matchers.define(:parse) do |input_text, options|
18
+ expected_output = nil
19
+ validator_block = nil
20
+ actual_result = nil
21
+ error_trace = nil
22
+
23
+ match do |parser_instance|
24
+ actual_result = parser_instance.parse(input_text)
25
+ if validator_block
26
+ validator_block.call(actual_result)
27
+ else
28
+ expected_output.nil? || expected_output == actual_result
29
+ end
30
+ rescue Parsanol::ParseFailed => e
31
+ error_trace = e.parse_failure_cause.ascii_tree if options && options[:trace]
32
+ false
33
+ end
34
+
35
+ failure_message do |parser_instance|
36
+ if validator_block
37
+ "expected output of parsing #{input_text.inspect} with " \
38
+ "#{parser_instance.inspect} to meet block conditions, but it didn't"
39
+ else
40
+ msg = if expected_output
41
+ "expected output of parsing #{input_text.inspect} with " \
42
+ "#{parser_instance.inspect} to equal #{expected_output.inspect}, " \
43
+ "but was #{actual_result.inspect}"
44
+ else
45
+ "expected #{parser_instance.inspect} to be able to parse " \
46
+ "#{input_text.inspect}"
47
+ end
48
+ msg += "\n#{error_trace}" if error_trace
49
+ msg
50
+ end
51
+ end
52
+
53
+ failure_message_when_negated do |parser_instance|
54
+ if validator_block
55
+ "expected output of parsing #{input_text.inspect} with " \
56
+ "#{parser_instance.inspect} not to meet block conditions, but it did"
57
+ elsif expected_output
58
+ "expected output of parsing #{input_text.inspect} with " \
59
+ "#{parser_instance.inspect} not to equal #{expected_output.inspect}"
60
+ else
61
+ "expected #{parser_instance.inspect} to not parse " \
62
+ "#{input_text.inspect}, but it did"
63
+ end
64
+ end
65
+
66
+ # Chain method for specifying expected output or validation block
67
+ chain :as do |expected = nil, &block|
68
+ expected_output = expected
69
+ validator_block = block
70
+ end
71
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ # Rope data structure for efficient string accumulation.
5
+ #
6
+ # Uses deferred concatenation to avoid O(n²) repeated string building.
7
+ # Segments are accumulated in O(1) time and joined once in O(n) time when
8
+ # converted to a final string.
9
+ #
10
+ # @example Basic usage
11
+ # rope = Rope.new
12
+ # rope.append('hello')
13
+ # rope.append(' ')
14
+ # rope.append('world')
15
+ # rope.to_s # => "hello world"
16
+ #
17
+ # @example With Slices
18
+ # rope = Rope.new
19
+ # rope.append(Slice.new(0, 'hello'))
20
+ # rope.append(Slice.new(5, ' world'))
21
+ # rope.to_s # => "hello world"
22
+ #
23
+ class Rope
24
+ # Creates a new empty Rope.
25
+ def initialize
26
+ @segments = []
27
+ @frozen = false
28
+ end
29
+
30
+ # Appends a string or Slice to the rope.
31
+ #
32
+ # This is an O(1) operation. The segment is stored as-is and will be
33
+ # joined later when {#to_s} is called.
34
+ #
35
+ # @param segment [String, Slice] The segment to append
36
+ # @return [Rope] self for method chaining
37
+ # @raise [FrozenError] if rope has been frozen by calling {#to_s}
38
+ def append(segment)
39
+ raise FrozenError, "can't modify frozen Rope" if @frozen
40
+
41
+ @segments << segment
42
+ self
43
+ end
44
+
45
+ # Converts the rope to a final string.
46
+ #
47
+ # This is an O(n) operation performed once. After calling this method,
48
+ # the rope is frozen and cannot be modified further.
49
+ #
50
+ # @return [String] The concatenated result of all segments
51
+ def to_s
52
+ @frozen = true
53
+ @segments.join
54
+ end
55
+
56
+ # Checks if the rope is empty (contains no segments).
57
+ #
58
+ # @return [Boolean] true if no segments have been appended
59
+ def empty?
60
+ @segments.empty?
61
+ end
62
+
63
+ # Estimates the total size of all segments.
64
+ #
65
+ # This is an estimate because segments may be Slice objects or other
66
+ # types that respond to #size or #to_s.
67
+ #
68
+ # @return [Integer] The sum of all segment sizes
69
+ def size
70
+ @segments.sum { |s| s.respond_to?(:size) ? s.size : s.to_s.size }
71
+ end
72
+
73
+ # Creates a rope from an existing string.
74
+ #
75
+ # @param str [String] The string to initialize the rope with
76
+ # @return [Rope] A new rope containing the string
77
+ def self.from_string(str)
78
+ new.tap { |r| r.append(str) unless str.empty? }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Scoped variable bindings for parser context management. Provides a
4
+ # stack-like interface for creating nested scopes that inherit from
5
+ # parent scopes.
6
+ #
7
+ # @example Basic usage
8
+ # scope = Parsanol::Scope.new
9
+ # scope[:x] = 1
10
+ # scope.push # Create nested scope
11
+ # scope[:x] # => 1 (inherited from parent)
12
+ # scope[:y] = 2
13
+ # scope.pop # Return to parent scope
14
+ # scope[:y] # raises NotFound
15
+ #
16
+ # Inspired by lexical scoping patterns in programming languages.
17
+ #
18
+ module Parsanol
19
+ class Scope
20
+ # Error raised when attempting to access an undefined binding.
21
+ class UndefinedVariable < StandardError
22
+ end
23
+ # Legacy alias for backward compatibility
24
+ NotFound = UndefinedVariable
25
+
26
+ # Internal class representing a single scope level. Each frame can
27
+ # look up values in its parent frame if not found locally.
28
+ class Frame
29
+ # @return [Frame, nil] parent frame in the scope chain
30
+ attr_reader :parent_frame
31
+
32
+ # Creates a new frame optionally linked to a parent.
33
+ #
34
+ # @param parent [Frame, nil] the parent frame to inherit from
35
+ def initialize(parent = nil)
36
+ @parent_frame = parent
37
+ @bindings = {}
38
+ end
39
+
40
+ # Retrieves a value by key, searching parent frames if necessary.
41
+ #
42
+ # @param key [Symbol] the variable name to look up
43
+ # @return [Object] the bound value
44
+ # @raise [UndefinedVariable] if key not found in any frame
45
+ def fetch(key)
46
+ if @bindings.key?(key)
47
+ @bindings[key]
48
+ elsif @parent_frame
49
+ @parent_frame.fetch(key)
50
+ else
51
+ raise UndefinedVariable, "No binding for #{key.inspect}"
52
+ end
53
+ end
54
+
55
+ # Stores a value in the current frame.
56
+ #
57
+ # @param key [Symbol] the variable name
58
+ # @param value [Object] the value to bind
59
+ # @return [Object] the stored value
60
+ def store(key, value)
61
+ @bindings[key] = value
62
+ end
63
+
64
+ alias [] fetch
65
+ alias []= store
66
+ end
67
+
68
+ # Creates a new scope with an empty root frame.
69
+ def initialize
70
+ @active_frame = Frame.new
71
+ end
72
+
73
+ # Retrieves a value from the current scope chain.
74
+ #
75
+ # @param key [Symbol] the variable name
76
+ # @return [Object] the bound value
77
+ # @raise [UndefinedVariable] if not found
78
+ def [](key)
79
+ @active_frame.fetch(key)
80
+ end
81
+
82
+ # Stores a value in the current frame.
83
+ #
84
+ # @param key [Symbol] the variable name
85
+ # @param value [Object] the value to bind
86
+ def []=(key, value)
87
+ @active_frame.store(key, value)
88
+ end
89
+
90
+ # Creates a new nested scope frame. Call #pop to restore.
91
+ #
92
+ # @return [void]
93
+ def push
94
+ @active_frame = Frame.new(@active_frame)
95
+ end
96
+
97
+ # Returns to the parent scope frame.
98
+ #
99
+ # @return [void]
100
+ def pop
101
+ @active_frame = @active_frame.parent_frame
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Source position tracker for parsed content.
4
+ # Preserves both the string value and its byte offset in the original input,
5
+ # enabling precise error reporting and source mapping.
6
+ #
7
+ # Line/column is computed LAZILY on first access — zero overhead
8
+ # for users who don't need position info.
9
+ module Parsanol
10
+ class Slice
11
+ include Parsanol::Resettable
12
+
13
+ attr_reader :content, :input
14
+
15
+ def initialize(byte_offset = 0, string_content = "", input = nil)
16
+ @byte_position = byte_offset
17
+ @content = string_content
18
+ @input = input
19
+ @line_and_column = nil
20
+ end
21
+
22
+ def reset!(new_offset = 0, new_content = "", new_input = nil)
23
+ @byte_position = new_offset
24
+ @content = new_content
25
+ @input = new_input
26
+ @line_and_column = nil
27
+ self
28
+ end
29
+
30
+ def self.from_rope(rope, offset, input = nil)
31
+ new(offset, rope.to_s, input)
32
+ end
33
+
34
+ # Position
35
+ def offset
36
+ @byte_position
37
+ end
38
+
39
+ alias bytepos offset
40
+ alias charpos offset
41
+ alias str content
42
+
43
+ # Equality
44
+ def ==(other)
45
+ return content == other if other.is_a?(String)
46
+ return content == other.content if other.is_a?(Parsanol::Slice)
47
+
48
+ content == other
49
+ end
50
+
51
+ def eql?(other)
52
+ other.is_a?(Parsanol::Slice) && content.eql?(other.content)
53
+ end
54
+
55
+ def hash
56
+ [content, offset].hash
57
+ end
58
+
59
+ # Delegated methods
60
+ def match(pattern)
61
+ content.match(pattern)
62
+ end
63
+
64
+ def size
65
+ content.size
66
+ end
67
+
68
+ alias length size
69
+
70
+ def empty?
71
+ content.empty?
72
+ end
73
+
74
+ def +(other)
75
+ self.class.new(@byte_position, content + other.to_s, @input)
76
+ end
77
+
78
+ # Lazy line/column — computed once and cached.
79
+ def line_and_column
80
+ raise ArgumentError, "Line/column requires input" unless @input
81
+
82
+ @line_and_column ||= compute_line_and_column
83
+ end
84
+
85
+ # Conversions
86
+ def to_str
87
+ content.to_s
88
+ end
89
+
90
+ alias to_s to_str
91
+
92
+ def to_slice
93
+ self
94
+ end
95
+
96
+ def to_sym
97
+ content.to_sym
98
+ end
99
+
100
+ def to_i
101
+ content.to_i
102
+ end
103
+
104
+ def to_f
105
+ content.to_f
106
+ end
107
+
108
+ def inspect
109
+ "#{content.inspect}@#{offset}"
110
+ end
111
+
112
+ # JSON
113
+ def to_json(*)
114
+ as_json.to_json(*)
115
+ end
116
+
117
+ def as_json
118
+ result = { "value" => content, "offset" => offset, "length" => length }
119
+ if @input
120
+ line, column = line_and_column
121
+ result["line"] = line
122
+ result["column"] = column
123
+ end
124
+ result
125
+ end
126
+
127
+ # Source span
128
+ def to_span(_input = nil)
129
+ line, column = line_and_column
130
+ end_line, end_column = line_and_column_at(offset + length)
131
+ start_pos = SourcePosition.new(offset: offset, line: line, column: column)
132
+ end_pos = SourcePosition.new(offset: offset + length, line: end_line,
133
+ column: end_column)
134
+ SourceSpan.new(start_pos: start_pos, end_pos: end_pos)
135
+ end
136
+
137
+ private
138
+
139
+ def compute_line_and_column
140
+ line_and_column_at(@byte_position)
141
+ end
142
+
143
+ # Unified line/column computation:
144
+ # - String input: compute from input string
145
+ # - LineCache: delegate to cache
146
+ def line_and_column_at(pos)
147
+ if @input.respond_to?(:line_and_column)
148
+ # LineCache or duck-typed object
149
+ @input.line_and_column(pos)
150
+ else
151
+ # String input
152
+ prefix = @input.byteslice(0, pos) || ""
153
+ line = 1 + prefix.count("\n")
154
+ last_nl = prefix.rindex("\n")
155
+ column = last_nl ? pos - last_nl : pos + 1
156
+ [line, column]
157
+ end
158
+ end
159
+ end
160
+ end