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,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ast_visitor"
4
+
5
+ module Parsanol
6
+ module Optimizers
7
+ # Optimizes sequence patterns in the AST
8
+ # Follows visitor pattern for clean separation of concerns
9
+ #
10
+ # Transformations:
11
+ # - str('a') >> str('b') => str('ab') (merge adjacent strings)
12
+ # - (A >> B) >> C => A >> B >> C (flatten nested sequences)
13
+ # - Sequence(A) => A (unwrap single-element sequences)
14
+ class SequenceOptimizer < ASTVisitor
15
+ # Visit a sequence node and apply sequence optimizations
16
+ # @param parslet [Parsanol::Atoms::Sequence] sequence to optimize
17
+ # @return [Parsanol::Atoms::Base] optimized parslet
18
+ def visit_sequence(parslet)
19
+ # First optimize children recursively
20
+ new_parslets = parslet.parslets.map { |p| visit(p) }
21
+
22
+ # Optimization 1: Flatten nested sequences
23
+ flattened = flatten_sequences(new_parslets)
24
+
25
+ # Optimization 2: Merge adjacent string literals
26
+ merged = merge_adjacent_strings(flattened)
27
+
28
+ # Optimization 3: Unwrap single-element sequences
29
+ return merged[0] if merged.size == 1
30
+
31
+ # Return optimized sequence if changed
32
+ if merged == parslet.parslets
33
+ parslet
34
+ else
35
+ Parsanol::Atoms::Sequence.new(*merged)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # Flatten nested sequences into a single level
42
+ # @param parslets [Array<Parsanol::Atoms::Base>] array of parslets
43
+ # @return [Array<Parsanol::Atoms::Base>] flattened array
44
+ def flatten_sequences(parslets)
45
+ result = []
46
+ parslets.each do |p|
47
+ if p.is_a?(Parsanol::Atoms::Sequence)
48
+ result.concat(p.parslets)
49
+ else
50
+ result << p
51
+ end
52
+ end
53
+ result
54
+ end
55
+
56
+ # Merge adjacent Str atoms into single Str atoms
57
+ # @param parslets [Array<Parsanol::Atoms::Base>] array of parslets
58
+ # @return [Array<Parsanol::Atoms::Base>] array with merged strings
59
+ def merge_adjacent_strings(parslets)
60
+ return parslets if parslets.size < 2
61
+
62
+ result = []
63
+ i = 0
64
+
65
+ while i < parslets.size
66
+ current = parslets[i]
67
+
68
+ if current.is_a?(Parsanol::Atoms::Str)
69
+ # Look ahead for consecutive Str atoms using Rope for O(1) append
70
+ rope = Parsanol::Rope.new.append(current.str)
71
+ j = i + 1
72
+
73
+ while j < parslets.size && parslets[j].is_a?(Parsanol::Atoms::Str)
74
+ rope.append(parslets[j].str)
75
+ j += 1
76
+ end
77
+
78
+ # Create merged Str if we found consecutive strings
79
+ # O(n) join happens once at the end instead of O(n²) repeated concatenation
80
+ if j > i + 1
81
+ result << Parsanol::Atoms::Str.new(rope.to_s)
82
+ i = j
83
+ else
84
+ result << current
85
+ i += 1
86
+ end
87
+ else
88
+ result << current
89
+ i += 1
90
+ end
91
+ end
92
+
93
+ result
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Parsanol::ZeroCopy - Zero-Copy Transform Mode (Direct FFI Object Construction)
4
+ #
5
+ # This mode provides MAXIMUM PERFORMANCE through zero-copy FFI.
6
+ # - Rust directly constructs Ruby objects via rb_class_new, rb_ivar_set
7
+ # - No serialization overhead whatsoever
8
+ # - REQUIRES native extension AND type mapping definitions
9
+ #
10
+ # Usage:
11
+ # # Define Ruby classes that mirror Rust types
12
+ # module Calculator
13
+ # class Number < Expr
14
+ # attr_reader :value
15
+ # def initialize(value); @value = value; end
16
+ # def eval = @value
17
+ # end
18
+ #
19
+ # class BinOp < Expr
20
+ # attr_reader :left, :op, :right
21
+ # def eval; ...; end
22
+ # end
23
+ # end
24
+ #
25
+ # class CalculatorParser < Parsanol::Parser
26
+ # include Parsanol::ZeroCopy
27
+ #
28
+ # rule(:number) { ... }
29
+ # root(:expression)
30
+ #
31
+ # # Type mapping (tells Rust which Ruby classes to construct)
32
+ # output_types(
33
+ # number: Calculator::Number,
34
+ # binop: Calculator::BinOp
35
+ # )
36
+ # end
37
+ #
38
+ # parser = CalculatorParser.new
39
+ # expr = parser.parse("42+8") # Returns Calculator::Number or BinOp DIRECTLY
40
+ # puts expr.eval # No transform needed!
41
+ #
42
+ # Performance: FASTEST mode (18-44x faster than pure Ruby)
43
+ # Memory: Lowest overhead (zero-copy, no serialization)
44
+
45
+ module Parsanol
46
+ module ZeroCopy
47
+ def self.included(base)
48
+ base.extend(ClassMethods)
49
+ end
50
+
51
+ module ClassMethods
52
+ # Define output type mapping for zero-copy construction
53
+ #
54
+ # This tells the Rust parser which Ruby classes to instantiate
55
+ # for each named capture in the grammar.
56
+ #
57
+ # @param types [Hash] Mapping of rule names to Ruby classes
58
+ # @example
59
+ # output_types(
60
+ # number: Calculator::Number,
61
+ # binop: Calculator::BinOp,
62
+ # expr: Calculator::Expr
63
+ # )
64
+ def output_types(types = nil)
65
+ @output_types = types if types
66
+ @output_types ||= {}
67
+ end
68
+
69
+ # Define a single output type mapping
70
+ #
71
+ # @param rule_name [Symbol, String] Name of the rule
72
+ # @param ruby_class [Class] Ruby class to instantiate
73
+ # @example
74
+ # output_type :number, Calculator::Number
75
+ def output_type(rule_name, ruby_class)
76
+ output_types[rule_name.to_sym] = ruby_class
77
+ end
78
+
79
+ # Get output types as a hash suitable for FFI
80
+ #
81
+ # @return [Hash] String keys, class names as values
82
+ def output_types_for_ffi
83
+ output_types.transform_keys(&:to_s).transform_values do |klass|
84
+ klass.is_a?(Class) ? klass.name : klass.to_s
85
+ end
86
+ end
87
+ end
88
+
89
+ # Parse input and return direct Ruby objects (no serialization)
90
+ #
91
+ # @param input [String] The input string to parse
92
+ # @return [Object] Direct Ruby object (type depends on grammar)
93
+ # @raise [LoadError] If native extension not available
94
+ # @raise [Parsanol::ParseFailed] If parsing fails
95
+ def parse(input)
96
+ unless Parsanol::Native.available?
97
+ raise LoadError,
98
+ "ZeroCopy mode requires native extension for direct FFI object construction. " \
99
+ "Run `rake compile` to build the extension, or use " \
100
+ "parser.parse(input, mode: :ruby) for pure Ruby parsing."
101
+ end
102
+
103
+ grammar_json = Parsanol::Native.serialize_grammar(root)
104
+ type_map = self.class.output_types_for_ffi
105
+
106
+ if type_map.empty?
107
+ raise ArgumentError,
108
+ "ZeroCopy mode requires output_types to be defined. " \
109
+ "Add `output_types(number: MyNumberClass)` to your parser class."
110
+ end
111
+
112
+ Parsanol::Native.parse_to_objects(grammar_json, input, type_map)
113
+ end
114
+
115
+ # Parse with explicit type map override
116
+ #
117
+ # @param input [String] The input string to parse
118
+ # @param type_map [Hash] Override type mapping for this parse
119
+ # @return [Object] Direct Ruby object
120
+ def parse_with_types(input, type_map)
121
+ raise LoadError, "ZeroCopy mode requires native extension." unless Parsanol::Native.available?
122
+
123
+ grammar_json = Parsanol::Native.serialize_grammar(root)
124
+ Parsanol::Native.parse_to_objects(grammar_json, input, type_map)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Parsanol Transform Mode Options
4
+ #
5
+ # This module provides the ZeroCopy transformation mode for maximum performance:
6
+ #
7
+ # ZeroCopy - Direct FFI object construction (requires native extension, fastest)
8
+ #
9
+ # Usage:
10
+ # class MyParser < Parsanol::Parser
11
+ # include Parsanol::ZeroCopy
12
+ # rule(:number) { match('[0-9]').repeat(1).as(:int) }
13
+ # root(:number)
14
+ #
15
+ # output_types(number: MyNumberClass)
16
+ # end
17
+ #
18
+ # For standard parsing, use the Parse Modes API instead:
19
+ # parser.parse(input, mode: :native) # or :ruby, :json
20
+
21
+ require "parsanol/options/zero_copy"
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ # Parallel parsing support for batch processing multiple inputs.
5
+ # Uses rayon for linear speedup on multi-core systems.
6
+ #
7
+ # @example Parse multiple files in parallel
8
+ # grammar = MyParser.new.serialize_grammar
9
+ # inputs = Dir.glob("*.json").map { |f| File.read(f) }
10
+ #
11
+ # results = Parsanol::Parallel.parse_batch(grammar, inputs)
12
+ # results.each_with_index do |result, i|
13
+ # case result
14
+ # when Hash then puts "File #{i} parsed: #{result}"
15
+ # when Parsanol::ParseFailed then puts "File #{i} failed: #{result.message}"
16
+ # end
17
+ # end
18
+ #
19
+ module Parallel
20
+ # Configuration for parallel parsing.
21
+ #
22
+ # @example Configure with 8 threads
23
+ # config = Parsanol::Parallel::Config.new
24
+ # .with_num_threads(8)
25
+ # .with_min_chunk_size(50)
26
+ #
27
+ # results = Parsanol::Parallel.parse_batch(grammar, inputs, config: config)
28
+ #
29
+ class Config
30
+ # @return [Integer, nil] Number of threads (nil = auto-detect based on CPU cores)
31
+ attr_accessor :num_threads
32
+
33
+ # @return [Integer] Minimum inputs per thread (default: 10)
34
+ attr_accessor :min_chunk_size
35
+
36
+ def initialize
37
+ @num_threads = nil # Auto-detect
38
+ @min_chunk_size = 10
39
+ end
40
+
41
+ # Set the number of threads to use.
42
+ #
43
+ # @param n [Integer] Number of threads
44
+ # @return [Config] self for chaining
45
+ def with_num_threads(n)
46
+ @num_threads = n
47
+ self
48
+ end
49
+
50
+ # Set the minimum chunk size per thread.
51
+ #
52
+ # @param size [Integer] Minimum inputs per thread
53
+ # @return [Config] self for chaining
54
+ def with_min_chunk_size(size)
55
+ @min_chunk_size = size
56
+ self
57
+ end
58
+ end
59
+
60
+ class << self
61
+ # Parse multiple inputs in parallel.
62
+ #
63
+ # When the native extension with parallel feature is available,
64
+ # this uses rayon for parallel execution. Otherwise, falls back
65
+ # to sequential parsing.
66
+ #
67
+ # @param grammar_json [String] JSON-serialized grammar
68
+ # @param inputs [Array<String>] Array of input strings to parse
69
+ # @param config [Config] Parallel configuration (optional)
70
+ # @return [Array<Object>] Array of parse results in same order as inputs
71
+ #
72
+ # @example Basic usage
73
+ # results = Parsanol::Parallel.parse_batch(grammar, inputs)
74
+ #
75
+ # @example With configuration
76
+ # config = Parsanol::Parallel::Config.new.with_num_threads(4)
77
+ # results = Parsanol::Parallel.parse_batch(grammar, inputs, config: config)
78
+ #
79
+ def parse_batch(grammar_json, inputs, config: Config.new)
80
+ unless Parsanol::Native.available?
81
+ raise LoadError,
82
+ "Parallel parsing requires native extension. " \
83
+ "Run `rake compile` to build the extension."
84
+ end
85
+
86
+ Parsanol::Native.parse_batch_parallel(
87
+ grammar_json,
88
+ inputs,
89
+ num_threads: config.num_threads,
90
+ )
91
+ end
92
+
93
+ # Parse multiple inputs in parallel with transformation.
94
+ #
95
+ # @param grammar_json [String] JSON-serialized grammar
96
+ # @param inputs [Array<String>] Array of input strings to parse
97
+ # @param transform [Parsanol::Transform] Transform to apply to each result
98
+ # @param config [Config] Parallel configuration (optional)
99
+ # @return [Array<Object>] Array of transformed results
100
+ #
101
+ def parse_batch_with_transform(grammar_json, inputs, transform,
102
+ config: Config.new)
103
+ results = parse_batch(grammar_json, inputs, config: config)
104
+ results.map { |result| transform.apply(result) }
105
+ end
106
+
107
+ # Get the number of available CPU cores for parallel processing.
108
+ #
109
+ # @return [Integer] Number of available cores
110
+ def available_cores
111
+ require "etc"
112
+ Etc.nprocessors
113
+ rescue StandardError
114
+ 1
115
+ end
116
+
117
+ # Estimate optimal number of threads for a given input size.
118
+ #
119
+ # @param input_count [Integer] Number of inputs to process
120
+ # @return [Integer] Recommended number of threads
121
+ def optimal_threads(input_count)
122
+ cores = available_cores
123
+ # Don't use more threads than inputs
124
+ [cores, input_count].min
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for constructing PEG parsers. Provides a declarative DSL for
4
+ # defining grammar rules and designating the root (entry point) rule.
5
+ #
6
+ # @example Define a simple parser
7
+ # class NumberParser < Parsanol::Parser
8
+ # rule(:digit) { match('[0-9]') }
9
+ # rule(:number) { digit.repeat(1) }
10
+ # root(:number)
11
+ # end
12
+ #
13
+ # NumberParser.new.parse("123") # => "123"
14
+ #
15
+ # Parser classes can be embedded within other parsers, enabling grammar
16
+ # composition and reuse.
17
+ #
18
+ # @example Composing parsers
19
+ # class InnerParser < Parsanol::Parser
20
+ # root :inner
21
+ # rule(:inner) { str('x').repeat(3) }
22
+ # end
23
+ #
24
+ # class OuterParser < Parsanol::Parser
25
+ # root :outer
26
+ # rule(:outer) { str('a') >> InnerParser.new >> str('a') }
27
+ # end
28
+ #
29
+ # OuterParser.new.parse("axxxa") # => "axxxa"
30
+ #
31
+ # Inspired by parser combinator and parsing expression grammar patterns.
32
+ #
33
+ module Parsanol
34
+ class Parser < Parsanol::Atoms::Base
35
+ include Parsanol
36
+
37
+ class << self
38
+ # Declares the root (entry point) rule for this parser.
39
+ # The root is where parsing begins when #parse is called.
40
+ #
41
+ # @param rule_name [Symbol] name of the rule to use as root
42
+ #
43
+ # @example
44
+ # class MyParser < Parsanol::Parser
45
+ # rule(:document) { ... }
46
+ # root(:document) # parsing starts here
47
+ # end
48
+ #
49
+ def root(rule_name)
50
+ # Remove any existing root method before redefining
51
+ undef_method :root if method_defined?(:root)
52
+ define_method(:root) { __send__(rule_name) }
53
+ end
54
+ end
55
+
56
+ # Delegates matching to the root rule.
57
+ #
58
+ # @param src [Parsanol::Source] input source
59
+ # @param ctx [Parsanol::Atoms::Context] parsing context
60
+ # @param should_consume_all [Boolean] require complete consumption
61
+ # @return [Array(Boolean, Object)] parse result
62
+ #
63
+ def try(src, ctx, should_consume_all)
64
+ root.try(src, ctx, should_consume_all)
65
+ end
66
+
67
+ # Formats this parser for display (delegates to root rule).
68
+ #
69
+ # @param prec [Integer] precedence level
70
+ # @return [String] formatted representation
71
+ #
72
+ def to_s_inner(prec)
73
+ root.to_s(prec)
74
+ end
75
+
76
+ # Entry point for visitor traversal from parser root.
77
+ #
78
+ # @param visitor [Object] visitor object
79
+ def accept(visitor)
80
+ visitor.visit_parser(root)
81
+ end
82
+
83
+ # Unified parsing interface with mode selection support.
84
+ #
85
+ # All parse modes return results with Slice objects that contain
86
+ # position information (offset, length, line, column). This enables
87
+ # source code extraction, error reporting, and remark attachment.
88
+ #
89
+ # @param input [String] the string to parse
90
+ # @param mode_or_opts [Symbol, Hash] parsing mode or options hash
91
+ # @param kwargs [Hash] additional keyword options
92
+ #
93
+ # Modes:
94
+ # - :ruby - Pure Ruby parsing (always available, returns Slices with position)
95
+ # - :native - Use Rust extension if available, fallback to Ruby
96
+ # - :json - Return JSON string with position info for each value
97
+ #
98
+ # Options:
99
+ # - :reporter - Custom error reporter instance
100
+ # - :prefix - Allow partial matching (default: false)
101
+ #
102
+ # @return [Hash, Array, Parsanol::Slice] parsed result with position info
103
+ # @raise [Parsanol::ParseFailed] when parsing fails
104
+ #
105
+ # @example Parse and access position info
106
+ # result = parser.parse("hello")
107
+ # result[:name].offset # => 0
108
+ # result[:name].line_and_column # => [1, 1]
109
+ # result[:name].to_s # => "hello"
110
+ #
111
+ def parse(input, mode_or_opts = {}, **kwargs)
112
+ if mode_or_opts.is_a?(Hash) && !kwargs.key?(:mode)
113
+ # Legacy API: parse(input, options={})
114
+ merged = mode_or_opts.merge(kwargs)
115
+ if Parsanol::Native.available? && !merged.key?(:prefix) &&
116
+ !merged.key?(:reporter)
117
+ # Native backend by default; falls back to pure Ruby inside
118
+ # parse_native when the extension is missing.
119
+ parse_native(input, merged)
120
+ else
121
+ super(input, merged)
122
+ end
123
+ else
124
+ # New API: parse(input, mode:, **options)
125
+ mode = kwargs.delete(:mode) ||
126
+ (Parsanol::Native.available? ? :native : :ruby)
127
+ case mode
128
+ when :ruby
129
+ super(input, kwargs)
130
+ when :native
131
+ parse_native(input, kwargs)
132
+ when :json
133
+ parse_json(input, kwargs)
134
+ else
135
+ raise ArgumentError,
136
+ "Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
137
+ end
138
+ end
139
+ end
140
+
141
+ # Parses multiple inputs in batch mode.
142
+ #
143
+ # @param inputs [Array<String>] strings to parse
144
+ # @param mode [Symbol] parsing mode (:ruby, :native, or :json)
145
+ # @param opts [Hash] additional options
146
+ # @return [Array] array of parse results
147
+ #
148
+ def parse_batch(inputs, mode: :ruby, **opts)
149
+ inputs.map { |str| parse(str, mode: mode, **opts) }
150
+ end
151
+
152
+ # Clear the Rust grammar cache to free memory.
153
+ #
154
+ # @return [nil]
155
+ # @raise [LoadError] if native parser is not available
156
+ def clear_grammar_cache
157
+ Parsanol::Native.clear_grammar_cache
158
+ end
159
+
160
+ # Get the current number of cached grammars in Rust.
161
+ #
162
+ # @return [Integer] number of cached grammars
163
+ # @raise [LoadError] if native parser is not available
164
+ def grammar_cache_size
165
+ Parsanol::Native.grammar_cache_size
166
+ end
167
+
168
+ # Get the grammar cache capacity.
169
+ #
170
+ # @return [Integer] maximum cache capacity
171
+ # @raise [LoadError] if native parser is not available
172
+ def grammar_cache_capacity
173
+ Parsanol::Native.grammar_cache_capacity
174
+ end
175
+
176
+ # Get cache statistics for both Ruby and Rust caches.
177
+ #
178
+ # @return [Hash] cache statistics including Ruby GRAMMAR_CACHE and Rust grammar cache
179
+ # @raise [LoadError] if native parser is not available for Rust stats
180
+ def cache_stats
181
+ Parsanol::Native.cache_stats
182
+ end
183
+
184
+ private
185
+
186
+ # Dispatches to the appropriate parsing backend based on mode.
187
+ #
188
+ # @param mode [Symbol] the parsing mode
189
+ # @param input [String] input to parse
190
+ # @param opts [Hash] parsing options
191
+ # @return [Object] parse result
192
+ #
193
+ def dispatch_parse(mode, input, opts)
194
+ case mode
195
+ when :ruby
196
+ # Call base class parse directly (send needed since parse is defined in parent)
197
+ Parsanol::Atoms::Base.instance_method(:parse).bind_call(self, input,
198
+ opts)
199
+ when :native
200
+ parse_native(input, opts)
201
+ when :json
202
+ parse_json(input, opts)
203
+ else
204
+ raise ArgumentError,
205
+ "Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
206
+ end
207
+ end
208
+
209
+ # Native extension parsing with Ruby fallback.
210
+ # Returns results with Slice objects containing position info.
211
+ #
212
+ # @param input [String] input to parse
213
+ # @param opts [Hash] parsing options
214
+ # @return [Object] parse result with Slice objects for position info
215
+ #
216
+ def parse_native(input, opts)
217
+ if Parsanol::Native.available?
218
+ Parsanol::Native.parse(root, input)
219
+ else
220
+ Parsanol::Atoms::Base.instance_method(:parse).bind_call(self, input,
221
+ opts)
222
+ end
223
+ end
224
+
225
+ # JSON output mode - returns JSON with position info.
226
+ # All Slice values are serialized with their position information.
227
+ #
228
+ # @param input [String] input to parse
229
+ # @param opts [Hash] parsing options
230
+ # @return [String] JSON representation with position info
231
+ #
232
+ def parse_json(input, opts)
233
+ if Parsanol::Native.available?
234
+ grammar_def = Parsanol::Native.serialize_grammar(root)
235
+ outcome = Parsanol::Native.parse(grammar_def, input)
236
+ outcome.to_json
237
+ else
238
+ parse_ruby(input, opts).to_json
239
+ end
240
+ end
241
+ end
242
+ end