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,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fast mode patch for Parslet - matches vanilla parslet 2.0 behavior.
4
+ #
5
+ # For grammars with many small allocations (like EXPRESS), this is faster
6
+ # because the overhead of pool management exceeds the benefit.
7
+ #
8
+ # Usage:
9
+ # require 'parslet'
10
+ # require 'parsanol/fast_mode'
11
+ # # Now all parsing uses fast mode methods
12
+ #
13
+
14
+ module Parsanol
15
+ FAST_MODE = true
16
+
17
+ module Atoms
18
+ # Fast mode Context - matches vanilla parslet 2.0 simplicity
19
+ class Context
20
+ # Override try_with_cache with vanilla-like version (no eviction, no pooling)
21
+ def try_with_cache(obj, source, consume_all)
22
+ beg = source.bytepos
23
+
24
+ # Not in cache yet? Return early.
25
+ unless (entry = @cache[beg]&.[](obj.object_id))
26
+ result = obj.try(source, self, consume_all)
27
+
28
+ if obj.cached?
29
+ (@cache[beg] ||= {})[obj.object_id] =
30
+ [result, source.bytepos - beg]
31
+ end
32
+
33
+ return result
34
+ end
35
+
36
+ # Cache hit
37
+ result, advance = entry
38
+ source.bytepos = beg + advance
39
+ result
40
+ end
41
+ end
42
+
43
+ # Fast mode Sequence - direct array creation, no lazy evaluation
44
+ class Sequence
45
+ def try(source, context, consume_all)
46
+ parslets = @parslets
47
+
48
+ case parslets.size
49
+ when 1
50
+ success, value = parslets[0].apply(source, context, consume_all)
51
+ if success
52
+ succ([:sequence,
53
+ value])
54
+ else
55
+ context.err(self, source, @error_msg,
56
+ [value])
57
+ end
58
+ when 2
59
+ success, v1 = parslets[0].apply(source, context, false)
60
+ return context.err(self, source, @error_msg, [v1]) unless success
61
+
62
+ success, v2 = parslets[1].apply(source, context, consume_all)
63
+ if success
64
+ succ([:sequence, v1,
65
+ v2])
66
+ else
67
+ context.err(self, source, @error_msg, [v2])
68
+ end
69
+ when 3
70
+ success, v1 = parslets[0].apply(source, context, false)
71
+ return context.err(self, source, @error_msg, [v1]) unless success
72
+
73
+ success, v2 = parslets[1].apply(source, context, false)
74
+ return context.err(self, source, @error_msg, [v2]) unless success
75
+
76
+ success, v3 = parslets[2].apply(source, context, consume_all)
77
+ if success
78
+ succ([:sequence, v1, v2,
79
+ v3])
80
+ else
81
+ context.err(self, source, @error_msg, [v3])
82
+ end
83
+ else
84
+ result = [:sequence]
85
+ last_idx = parslets.size - 1
86
+ i = 0
87
+ while i <= last_idx
88
+ success, value = parslets[i].apply(source, context,
89
+ consume_all && i == last_idx)
90
+ return context.err(self, source, @error_msg, [value]) unless success
91
+
92
+ result << value
93
+ i += 1
94
+ end
95
+ succ(result)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Fast mode Repetition - direct array creation, no lazy evaluation
101
+ class Repetition
102
+ EMPTY_REPETITION_ARRAY = [:repetition].freeze
103
+
104
+ def try(source, context, consume_all)
105
+ parslet = @parslet
106
+ min = @min
107
+ max = @max
108
+ tag = @tag
109
+
110
+ # Fast path for .maybe
111
+ if min.zero? && max == 1
112
+ success, value = parslet.apply(source, context, false)
113
+ return succ([tag, value]) if success
114
+
115
+ return succ(tag == :repetition ? EMPTY_REPETITION_ARRAY : [tag])
116
+ end
117
+
118
+ # Fast path for exact count
119
+ if min == max && max && max <= 3
120
+ case max
121
+ when 1
122
+ success, value = parslet.apply(source, context, consume_all)
123
+ return success ? succ([tag,
124
+ value]) : context.err_at(self, source,
125
+ @error_msg, source.bytepos, [value])
126
+ when 2
127
+ success, v1 = parslet.apply(source, context, false)
128
+ unless success
129
+ return context.err_at(self, source, @error_msg, source.bytepos,
130
+ [v1])
131
+ end
132
+
133
+ success, v2 = parslet.apply(source, context, consume_all)
134
+ return success ? succ([tag, v1,
135
+ v2]) : context.err_at(self, source,
136
+ @error_msg, source.bytepos, [v2])
137
+ when 3
138
+ success, v1 = parslet.apply(source, context, false)
139
+ unless success
140
+ return context.err_at(self, source, @error_msg, source.bytepos,
141
+ [v1])
142
+ end
143
+
144
+ success, v2 = parslet.apply(source, context, false)
145
+ unless success
146
+ return context.err_at(self, source, @error_msg, source.bytepos,
147
+ [v2])
148
+ end
149
+
150
+ success, v3 = parslet.apply(source, context, consume_all)
151
+ return success ? succ([tag, v1, v2,
152
+ v3]) : context.err_at(self, source,
153
+ @error_msg, source.bytepos, [v3])
154
+ end
155
+ end
156
+
157
+ # General case
158
+ start_pos = source.bytepos
159
+ occ = 0
160
+ result = [tag]
161
+ break_on = nil
162
+
163
+ loop do
164
+ success, value = parslet.apply(source, context, false)
165
+ break_on = value
166
+ break unless success
167
+
168
+ occ += 1
169
+ result << value
170
+ break if max && occ >= max
171
+ end
172
+
173
+ if occ < min
174
+ source.bytepos = start_pos
175
+ return context.err_at(self, source, @error_msg, start_pos, [break_on])
176
+ end
177
+
178
+ if consume_all && source.chars_left.positive?
179
+ return context.err(self, source, @unconsumed_msg,
180
+ [break_on])
181
+ end
182
+
183
+ succ(result)
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # FIRST Set Analysis for PEG Grammars
4
+ #
5
+ # FIRST sets help identify which terminals can appear at the beginning of
6
+ # a parse. This is essential for:
7
+ # 1. Automatic cut operator insertion (AC-FIRST algorithm)
8
+ # 2. Grammar analysis and optimization
9
+ # 3. Detecting ambiguous choices
10
+ #
11
+ # Reference: Mizushima et al. (2010) "Packrat Parsers Can Handle Practical
12
+ # Grammars in Mostly Constant Space"
13
+ #
14
+ module Parsanol
15
+ module FirstSet
16
+ # Sentinel value representing the empty string (ε)
17
+ EPSILON = :epsilon
18
+
19
+ # Compute the FIRST set for this parslet atom
20
+ # Returns a Set containing:
21
+ # - Terminal atoms (Str, Re) that can match first
22
+ # - EPSILON if the atom can match empty string
23
+ # - nil elements represent unknown/variable terminals (e.g., any)
24
+ #
25
+ # @return [Set] FIRST set containing terminal atoms or EPSILON
26
+ def first_set
27
+ @first_set ||= compute_first_set
28
+ end
29
+
30
+ # Clear cached FIRST set (useful after grammar modifications)
31
+ def clear_first_set_cache
32
+ @first_set = nil
33
+ end
34
+
35
+ protected
36
+
37
+ # Override in subclasses to compute FIRST set
38
+ # Default: conservative approximation (unknown)
39
+ def compute_first_set
40
+ Set.new([nil]) # nil = unknown terminal
41
+ end
42
+
43
+ # Class methods for FIRST set analysis
44
+ class << self
45
+ # Check if two FIRST sets are disjoint
46
+ # Two sets are disjoint if they have no common elements
47
+ # EPSILON is ignored when checking disjointness
48
+ #
49
+ # @param set1 [Set] First FIRST set
50
+ # @param set2 [Set] Second FIRST set
51
+ # @return [Boolean] true if sets are disjoint
52
+ def disjoint?(set1, set2)
53
+ # Remove EPSILON and nil from both sets for comparison
54
+ real_set1 = set1.reject { |x| x == EPSILON || x.nil? }
55
+ real_set2 = set2.reject { |x| x == EPSILON || x.nil? }
56
+
57
+ # If either set is empty (only EPSILON/nil), consider disjoint
58
+ return true if real_set1.empty? || real_set2.empty?
59
+
60
+ # Check if intersection is empty (using to_a for Opal compatibility)
61
+ !real_set1.to_a.intersect?(real_set2.to_a)
62
+ end
63
+
64
+ # Check if all FIRST sets in a collection are mutually disjoint
65
+ # This is critical for AC-FIRST algorithm - we can only insert
66
+ # cuts when all alternatives have non-overlapping FIRST sets
67
+ #
68
+ # @param sets [Array<Set>] Collection of FIRST sets
69
+ # @return [Boolean] true if all pairs are disjoint
70
+ def all_disjoint?(sets)
71
+ # Need at least 2 sets to check disjointness
72
+ return true if sets.length < 2
73
+
74
+ # Check all pairs
75
+ sets.combination(2).all? { |s1, s2| disjoint?(s1, s2) }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Parsanol::GrammarBuilder - Grammar Composition
4
+ #
5
+ # Build complex grammars by importing and composing smaller grammars.
6
+ # This enables reusable grammar modules.
7
+ #
8
+ # Usage:
9
+ # # Define reusable grammars
10
+ # expression_grammar = GrammarBuilder.new
11
+ # .rule("expr", str("a") | str("b"))
12
+ # .build
13
+ #
14
+ # type_grammar = GrammarBuilder.new
15
+ # .rule("type", str("int") | str("str"))
16
+ # .build
17
+ #
18
+ # # Compose into a new grammar
19
+ # combined = GrammarBuilder.new
20
+ # .import(expression_grammar, prefix: "expr")
21
+ # .import(type_grammar, prefix: "type")
22
+ # .rule("typed", seq([ref("expr:root"), str(":"), ref("type:root")]))
23
+ # .build
24
+ #
25
+ # Requires native extension for full functionality.
26
+
27
+ module Parsanol
28
+ class GrammarBuilder
29
+ # Create a new grammar builder
30
+ def initialize
31
+ @rules = {}
32
+ @imports = []
33
+ @root = nil
34
+ end
35
+
36
+ # Define a rule
37
+ #
38
+ # @param name [String, Symbol] Rule name
39
+ # @param parslet [Parsanol::Atoms::Base] Parslet atom
40
+ # @return [self] For chaining
41
+ def rule(name, parslet)
42
+ @rules[name.to_s] = parslet
43
+ self
44
+ end
45
+
46
+ # Get a rule for modification
47
+ #
48
+ # @param name [String, Symbol] Rule name
49
+ # @return [Parsanol::Atoms::Base, nil] The rule atom
50
+ def [](name)
51
+ @rules[name.to_s]
52
+ end
53
+
54
+ # Set the root rule
55
+ #
56
+ # @param name [String, Symbol] Root rule name
57
+ # @return [self] For chaining
58
+ def root(name)
59
+ @root = name.to_s
60
+ self
61
+ end
62
+
63
+ # Import another grammar with optional prefix
64
+ #
65
+ # @param grammar [GrammarBuilder, Hash] Grammar to import
66
+ # @param prefix [String, nil] Optional prefix for imported rules
67
+ # @return [self] For chaining
68
+ def import(grammar, prefix: nil)
69
+ grammar_data = case grammar
70
+ when GrammarBuilder
71
+ grammar.to_h
72
+ when Hash
73
+ grammar
74
+ else
75
+ raise ArgumentError,
76
+ "Expected GrammarBuilder or Hash, got #{grammar.class}"
77
+ end
78
+
79
+ @imports << { grammar: grammar_data, prefix: prefix }
80
+ self
81
+ end
82
+
83
+ # Import with explicit rule mapping
84
+ #
85
+ # @param grammar [GrammarBuilder, Hash] Grammar to import
86
+ # @param prefix [String, nil] Optional prefix
87
+ # @param rules [Hash] Rule mapping {from_rule: to_rule}
88
+ # @return [self] For chaining
89
+ def import_with_rules(grammar, prefix: nil, rules: {})
90
+ grammar_data = case grammar
91
+ when GrammarBuilder
92
+ grammar.to_h
93
+ when Hash
94
+ grammar
95
+ else
96
+ raise ArgumentError,
97
+ "Expected GrammarBuilder or Hash, got #{grammar.class}"
98
+ end
99
+
100
+ @imports << { grammar: grammar_data, prefix: prefix, rules: rules }
101
+ self
102
+ end
103
+
104
+ # Build the grammar
105
+ #
106
+ # @return [Hash] Grammar representation
107
+ def build
108
+ {
109
+ rules: @rules,
110
+ root: @root,
111
+ imports: @imports,
112
+ }
113
+ end
114
+
115
+ # Convert to JSON for native parser
116
+ #
117
+ # @return [String] JSON representation
118
+ def to_json(*)
119
+ build.to_json
120
+ end
121
+
122
+ # Get as a Hash
123
+ #
124
+ # @return [Hash] Grammar representation
125
+ def to_h
126
+ build
127
+ end
128
+
129
+ # Reference another rule in this grammar
130
+ #
131
+ # @param name [String, Symbol] Rule name
132
+ # @return [Parsanol::Atoms::Entity] Entity referencing the rule
133
+ def ref(name)
134
+ Parsanol::Atoms::Entity.new(name)
135
+ end
136
+
137
+ # Reference the root of another grammar
138
+ #
139
+ # @param grammar_name [String] Name of the grammar (for prefixed imports)
140
+ # @return [Parsanol::Atoms::Entity] Entity referencing the root
141
+ def ref_root(grammar_name = nil)
142
+ if grammar_name
143
+ ref("#{grammar_name}:root")
144
+ else
145
+ ref("root")
146
+ end
147
+ end
148
+
149
+ class << self
150
+ # Create a grammar from a block
151
+ #
152
+ # @yield [GrammarBuilder] Builder to configure
153
+ # @return [Hash] Built grammar
154
+ def build(&)
155
+ builder = new
156
+ builder.instance_eval(&)
157
+ builder.build
158
+ end
159
+
160
+ # Import a grammar from JSON string
161
+ #
162
+ # @param json [String] JSON representation
163
+ # @return [Hash] Grammar representation
164
+ def from_json(json)
165
+ JSON.parse(json)
166
+ end
167
+ end
168
+ end
169
+
170
+ # Module methods for DSL
171
+ module GrammarBuilderDSL
172
+ # Create a new grammar builder
173
+ #
174
+ # @return [GrammarBuilder] New builder
175
+ def grammar(&)
176
+ GrammarBuilder.build(&)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Parsanol::IncrementalParser - Incremental Parser for Editor Integration
4
+ #
5
+ # Parse with support for incremental edits. This is useful for editor integration
6
+ # where the input changes frequently (e.g., as the user types).
7
+ #
8
+ # Usage:
9
+ # parser = Parsanol::IncrementalParser.new(grammar, initial_text)
10
+ #
11
+ # # When text changes
12
+ # parser.apply_edit(start: 5, deleted: 3, inserted: "new")
13
+ # result = parser.reparse
14
+ #
15
+ # Requires native extension for full functionality.
16
+
17
+ module Parsanol
18
+ # Represents an edit to apply to the input
19
+ class Edit
20
+ attr_reader :start, :deleted, :inserted
21
+
22
+ def initialize(start:, deleted:, inserted: "")
23
+ @start = start
24
+ @deleted = deleted
25
+ @inserted = inserted
26
+ end
27
+
28
+ # Get the old range that was replaced
29
+ def old_range
30
+ @start...(@start + @deleted)
31
+ end
32
+
33
+ # Check if this edit affects a specific position
34
+ def affects_position?(position)
35
+ position >= @start && position < @start + @deleted + @inserted.length
36
+ end
37
+
38
+ # Get the new position after this edit
39
+ def new_position
40
+ @start + @inserted.length
41
+ end
42
+
43
+ # Apply this edit to a string
44
+ def apply(input)
45
+ input[0...@start] + @inserted + input[(@start + @deleted)..]
46
+ end
47
+
48
+ def to_s
49
+ "Edit(#{@start}, +#{@inserted.length}, -#{@deleted})"
50
+ end
51
+
52
+ def ==(other)
53
+ return false unless other.is_a?(Edit)
54
+
55
+ @start == other.start && @deleted == other.deleted && @inserted == other.inserted
56
+ end
57
+ end
58
+
59
+ class IncrementalParser
60
+ # Create a new incremental parser
61
+ #
62
+ # @param grammar [Parsanol::Parser, Parsanol::Atoms::Base] Grammar to use
63
+ # @param initial_input [String] Initial input string
64
+ def initialize(grammar, initial_input = "")
65
+ @grammar = grammar
66
+ @input = initial_input
67
+
68
+ if Parsanol::Native.available?
69
+ grammar_json = Parsanol::Native.serialize_grammar(grammar.root)
70
+ @native_parser = Parsanol::Native.incremental_parser_new(grammar_json,
71
+ initial_input)
72
+ else
73
+ @native_parser = nil
74
+ end
75
+
76
+ @edits = []
77
+ @cached_result = nil
78
+ end
79
+
80
+ # Apply an edit to the parser
81
+ #
82
+ # @param start [Integer] Start position of edit
83
+ # @param deleted [Integer] Number of characters deleted
84
+ # @param inserted [String] Text to insert
85
+ def apply_edit(start:, deleted:, inserted: "")
86
+ edit = Edit.new(start: start, deleted: deleted, inserted: inserted)
87
+ @edits << edit
88
+
89
+ # Update cached input
90
+ @input = edit.apply(@input)
91
+
92
+ # Invalidate cached result
93
+ @cached_result = nil
94
+
95
+ return unless @native_parser
96
+
97
+ Parsanol::Native.incremental_parser_apply_edit(@native_parser, start,
98
+ deleted, inserted)
99
+ end
100
+
101
+ # Convenience method to apply multiple edits
102
+ #
103
+ # @param edits [Array<Hash>] Array of {start:, deleted:, inserted:} hashes
104
+ def apply_edits(edits)
105
+ edits.each do |edit_hash|
106
+ apply_edit(**edit_hash)
107
+ end
108
+ end
109
+
110
+ # Reparse with current input (or optional new input)
111
+ #
112
+ # @param new_input [String, nil] Optional new input (replaces current)
113
+ # @return [Object] Parse result
114
+ def reparse(new_input = nil)
115
+ if new_input
116
+ @input = new_input
117
+ @edits.clear
118
+ @cached_result = nil
119
+ end
120
+
121
+ return @cached_result if @cached_result
122
+
123
+ if @native_parser
124
+ @cached_result = Parsanol::Native.incremental_parser_reparse(
125
+ @native_parser, @input
126
+ )
127
+ else
128
+ # Pure Ruby fallback - reparse from scratch
129
+ root = @grammar.root
130
+ @cached_result = root.parse(@input)
131
+ end
132
+
133
+ @cached_result
134
+ end
135
+
136
+ # Invalidate a range (for external changes)
137
+ #
138
+ # @param start [Integer] Start position
139
+ # @param end_pos [Integer] End position
140
+ def invalidate_range(_start, _end_pos)
141
+ # Clear cached result if the invalidated range might affect it
142
+ @cached_result = nil
143
+
144
+ nil unless @native_parser
145
+ # Native implementation handles invalidation
146
+ end
147
+
148
+ # Get the current input
149
+ #
150
+ # @return [String] Current input
151
+ attr_reader :input
152
+
153
+ # Get all applied edits
154
+ #
155
+ # @return [Array<Edit>] Array of edits
156
+ def edits
157
+ @edits.dup
158
+ end
159
+
160
+ # Check if there are unapplied edits
161
+ #
162
+ # @return [Boolean] True if there are pending edits
163
+ def dirty?
164
+ @cached_result.nil? && !@edits.empty?
165
+ end
166
+
167
+ # Reset to initial state
168
+ #
169
+ # @param new_input [String, nil] Optional new initial input
170
+ def reset(new_input = nil)
171
+ @input = new_input || ""
172
+ @edits.clear
173
+ @cached_result = nil
174
+
175
+ return unless @native_parser && new_input
176
+
177
+ grammar_json = Parsanol::Native.serialize_grammar(@grammar.root)
178
+ @native_parser = Parsanol::Native.incremental_parser_new(grammar_json,
179
+ @input)
180
+ end
181
+ end
182
+ end