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
data/Rakefile ADDED
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ begin
6
+ require "rspec/core/rake_task"
7
+ rescue LoadError
8
+ # RSpec not available in this environment
9
+ end
10
+
11
+ require "rdoc/task"
12
+ require "rubygems/package_task"
13
+
14
+ begin
15
+ require "opal/rspec/rake_task"
16
+ rescue LoadError, NoMethodError
17
+ # Opal not available or incompatible with current Ruby version
18
+ end
19
+
20
+ GEMSPEC = Gem::Specification.load("parsanol.gemspec")
21
+
22
+ # Load rake tasks from rakelib/
23
+ Dir.glob("rakelib/*.rake").each { |r| load r }
24
+
25
+ desc "Run all tests"
26
+ RSpec::Core::RakeTask.new(:spec)
27
+
28
+ namespace :spec do
29
+ desc "Run unit tests only"
30
+ RSpec::Core::RakeTask.new(:unit) do |task|
31
+ task.pattern = "spec/parsanol/**/*_spec.rb"
32
+ end
33
+
34
+ if defined?(Opal::RSpec::RakeTask)
35
+ desc "Run Opal (JavaScript) tests"
36
+ Opal::RSpec::RakeTask.new(:opal) do |task|
37
+ task.append_path "lib"
38
+ end
39
+ end
40
+ end
41
+
42
+ RDoc::Task.new do |rdoc|
43
+ rdoc.rdoc_dir = "rdoc"
44
+ rdoc.title = "Parsanol"
45
+ rdoc.options << "--line-numbers"
46
+ rdoc.rdoc_files.include("README.adoc")
47
+ rdoc.rdoc_files.include("lib/**/*.rb")
48
+ end
49
+
50
+ desc "Print LOC statistics"
51
+ task :stat do
52
+ %w[lib spec example].each do |dir|
53
+ next unless Dir.exist?(dir)
54
+
55
+ loc = `find #{dir} -name "*.rb" | xargs wc -l | grep 'total'`.split.first.to_i
56
+ printf("%20s %d\n", dir, loc)
57
+ end
58
+ end
59
+
60
+ # ===== Native Gem Building =====
61
+ namespace :gem do
62
+ desc "Build source gem (compile on install)"
63
+ task "native:any" do
64
+ sh "rake gem:platform:any gem"
65
+ end
66
+
67
+ desc "Define the gem task to build on any platform (compile on install)"
68
+ task "platform:any" do
69
+ spec = Gem::Specification.load("parsanol.gemspec").dup
70
+ task = Gem::PackageTask.new(spec)
71
+ task.define
72
+ end
73
+ end
74
+
75
+ namespace :benchmark do
76
+ desc "Run comprehensive benchmark suite"
77
+ task :all do
78
+ ruby "benchmark/benchmark_suite.rb"
79
+ end
80
+
81
+ desc "Run example-focused benchmarks"
82
+ task :examples do
83
+ ruby "benchmark/example_benchmarks.rb"
84
+ end
85
+
86
+ desc "Run benchmarks and export results to JSON/YAML"
87
+ task :export do
88
+ ruby "benchmark/benchmark_runner.rb"
89
+ end
90
+
91
+ desc "Run quick benchmark (examples only)"
92
+ task quick: :examples
93
+ end
94
+
95
+ # Load comparative benchmark tasks
96
+ Dir.glob("benchmark/tasks/*.rake").each { |r| load r }
97
+
98
+ desc "Run quick benchmarks"
99
+ task benchmark: "benchmark:quick"
100
+
101
+ # ===== Parslet Compatibility Tests =====
102
+ namespace :compat do
103
+ desc "Run imported Parslet tests with original Parslet (baseline)"
104
+ task :parslet do
105
+ ENV["PARSANOL_BACKEND"] = "parslet"
106
+ sh "bundle exec rspec spec/parslet_imported/ --format documentation"
107
+ end
108
+
109
+ desc "Run imported Parslet tests with Parsanol compatibility layer"
110
+ task :parsanol do
111
+ ENV["PARSANOL_BACKEND"] = "parsanol"
112
+ sh "bundle exec rspec spec/parslet_imported/ --format documentation"
113
+ end
114
+
115
+ desc "Run both and save results for comparison"
116
+ task :compare do
117
+ require "fileutils"
118
+
119
+ results_dir = "tmp/compat_results"
120
+ FileUtils.mkdir_p(results_dir)
121
+
122
+ puts "=== Running with original Parslet ==="
123
+ ENV["PARSANOL_BACKEND"] = "parslet"
124
+ sh "bundle exec rspec spec/parslet_imported/ --format documentation > #{results_dir}/parslet.txt 2>&1"
125
+
126
+ puts "\n=== Running with Parsanol::Parslet ==="
127
+ ENV["PARSANOL_BACKEND"] = "parsanol"
128
+ sh "bundle exec rspec spec/parslet_imported/ --format documentation > #{results_dir}/parsanol.txt 2>&1"
129
+
130
+ puts "\n=== Comparing results ==="
131
+ puts "Results saved to:"
132
+ puts " - #{results_dir}/parslet.txt"
133
+ puts " - #{results_dir}/parsanol.txt"
134
+ puts "\nTo compare: diff #{results_dir}/parslet.txt #{results_dir}/parsanol.txt"
135
+ end
136
+
137
+ desc "Run imported Parslet tests (default: with Parsanol)"
138
+ task run: :parsanol
139
+ end
140
+
141
+ task default: :spec
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for AST visitors following the Visitor pattern
4
+ # This separates tree traversal logic from transformation logic
5
+ # making the code more maintainable and extensible.
6
+ module Parsanol
7
+ # Base visitor class that traverses the Parslet AST
8
+ # Subclasses override visit_* methods to perform transformations
9
+ class ASTVisitor
10
+ # Visit a parslet and its children
11
+ # Subclasses should override specific visit_* methods
12
+ # @param parslet [Parsanol::Atoms::Base] parslet to visit
13
+ # @return [Parsanol::Atoms::Base] transformed parslet
14
+ def visit(parslet)
15
+ case parslet
16
+ when Parsanol::Atoms::Sequence
17
+ visit_sequence(parslet)
18
+ when Parsanol::Atoms::Alternative
19
+ visit_alternative(parslet)
20
+ when Parsanol::Atoms::Repetition
21
+ visit_repetition(parslet)
22
+ when Parsanol::Atoms::Lookahead
23
+ visit_lookahead(parslet)
24
+ when Parsanol::Atoms::Named
25
+ visit_named(parslet)
26
+ when Parsanol::Atoms::Str
27
+ visit_str(parslet)
28
+ when Parsanol::Atoms::Re
29
+ visit_re(parslet)
30
+ else
31
+ # Leaf nodes or unknown types - return as-is
32
+ parslet
33
+ end
34
+ end
35
+
36
+ # Visit a sequence node
37
+ # Default implementation visits children and reconstructs if changed
38
+ # @param parslet [Parsanol::Atoms::Sequence] sequence to visit
39
+ # @return [Parsanol::Atoms::Base] transformed sequence
40
+ def visit_sequence(parslet)
41
+ new_parslets = parslet.parslets.map { |p| visit(p) }
42
+ if new_parslets == parslet.parslets
43
+ parslet
44
+ else
45
+ Parsanol::Atoms::Sequence.new(*new_parslets)
46
+ end
47
+ end
48
+
49
+ # Visit an alternative node
50
+ # Default implementation visits children and reconstructs if changed
51
+ # @param parslet [Parsanol::Atoms::Alternative] alternative to visit
52
+ # @return [Parsanol::Atoms::Base] transformed alternative
53
+ def visit_alternative(parslet)
54
+ new_alternatives = parslet.alternatives.map { |p| visit(p) }
55
+ if new_alternatives == parslet.alternatives
56
+ parslet
57
+ else
58
+ Parsanol::Atoms::Alternative.new(*new_alternatives)
59
+ end
60
+ end
61
+
62
+ # Visit a repetition node
63
+ # Default implementation visits child and reconstructs if changed
64
+ # @param parslet [Parsanol::Atoms::Repetition] repetition to visit
65
+ # @return [Parsanol::Atoms::Base] transformed repetition
66
+ def visit_repetition(parslet)
67
+ new_parslet = visit(parslet.parslet)
68
+ if new_parslet.equal?(parslet.parslet)
69
+ parslet
70
+ else
71
+ Parsanol::Atoms::Repetition.new(
72
+ new_parslet,
73
+ parslet.min,
74
+ parslet.max,
75
+ parslet.instance_variable_get(:@tag),
76
+ )
77
+ end
78
+ end
79
+
80
+ # Visit a lookahead node
81
+ # Default implementation visits child and reconstructs if changed
82
+ # @param parslet [Parsanol::Atoms::Lookahead] lookahead to visit
83
+ # @return [Parsanol::Atoms::Base] transformed lookahead
84
+ def visit_lookahead(parslet)
85
+ new_bound = visit(parslet.bound_parslet)
86
+ if new_bound.equal?(parslet.bound_parslet)
87
+ parslet
88
+ else
89
+ Parsanol::Atoms::Lookahead.new(new_bound, parslet.positive)
90
+ end
91
+ end
92
+
93
+ # Visit a named node
94
+ # Default implementation visits child and reconstructs if changed
95
+ # @param parslet [Parsanol::Atoms::Named] named to visit
96
+ # @return [Parsanol::Atoms::Base] transformed named
97
+ def visit_named(parslet)
98
+ new_parslet = visit(parslet.parslet)
99
+ if new_parslet.equal?(parslet.parslet)
100
+ parslet
101
+ else
102
+ Parsanol::Atoms::Named.new(new_parslet, parslet.name)
103
+ end
104
+ end
105
+
106
+ # Visit a string literal node
107
+ # Default implementation returns as-is (leaf node)
108
+ # @param parslet [Parsanol::Atoms::Str] string to visit
109
+ # @return [Parsanol::Atoms::Base] transformed string
110
+ def visit_str(parslet)
111
+ parslet
112
+ end
113
+
114
+ # Visit a regex node
115
+ # Default implementation returns as-is (leaf node)
116
+ # @param parslet [Parsanol::Atoms::Re] regex to visit
117
+ # @return [Parsanol::Atoms::Base] transformed regex
118
+ def visit_re(parslet)
119
+ parslet
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ordered choice - tries alternatives left-to-right, returning first success.
4
+ # Fails only if all alternatives fail.
5
+ #
6
+ # @example Simple choice
7
+ # str('a') | str('b') # matches 'a' or 'b'
8
+ #
9
+ # This is PEG ordered choice - no backtracking to later alternatives.
10
+ #
11
+ module Parsanol
12
+ module Atoms
13
+ class Alternative < Parsanol::Atoms::Base
14
+ # @return [Array<Parsanol::Atoms::Base>] alternative parsers
15
+ attr_reader :alternatives
16
+
17
+ # Creates a new choice.
18
+ #
19
+ # @param options [Array<Parsanol::Atoms::Base>] alternatives
20
+ def initialize(*options)
21
+ super()
22
+ @alternatives = options
23
+ @choice_error = "Expected one of #{options.inspect}"
24
+ end
25
+
26
+ # Adds an alternative with flattening.
27
+ #
28
+ # @param parser [Parsanol::Atoms::Base] new alternative
29
+ # @return [Parsanol::Atoms::Alternative] flattened choice
30
+ def |(other)
31
+ expanded = if other.is_a?(Parsanol::Atoms::Alternative)
32
+ @alternatives + other.alternatives
33
+ else
34
+ @alternatives + [other]
35
+ end
36
+ self.class.new(*expanded)
37
+ end
38
+
39
+ # Tries each alternative in order.
40
+ #
41
+ # @param source [Parsanol::Source] input
42
+ # @param context [Parsanol::Atoms::Context] context
43
+ # @param consume_all [Boolean] require full consumption
44
+ # @return [Array(Boolean, Object)] result
45
+ def try(source, context, consume_all)
46
+ options = @alternatives
47
+ count = options.size
48
+
49
+ # Optimized paths for common sizes
50
+ case count
51
+ when 2
52
+ try_two(options[0], options[1], source, context, consume_all)
53
+ when 3
54
+ try_three(options[0], options[1], options[2], source, context,
55
+ consume_all)
56
+ else
57
+ try_many(options, source, context, consume_all)
58
+ end
59
+ end
60
+
61
+ precedence CHOICE
62
+
63
+ # String representation.
64
+ #
65
+ # @param prec [Integer] precedence
66
+ # @return [String]
67
+ def to_s_inner(prec)
68
+ @alternatives.map { |a| a.to_s(prec) }.join(" / ")
69
+ end
70
+
71
+ # FIRST set is union of all alternatives' FIRST sets.
72
+ #
73
+ # @return [Set]
74
+ def compute_first_set
75
+ return Set.new if @alternatives.empty?
76
+
77
+ @alternatives.map(&:first_set).reduce(&:union)
78
+ end
79
+
80
+ private
81
+
82
+ # Two-alternative fast path
83
+ def try_two(a1, a2, source, context, consume_all)
84
+ success, value1 = a1.apply(source, context, consume_all)
85
+ return [success, value1] if success
86
+
87
+ success, value2 = a2.apply(source, context, consume_all)
88
+ return [success, value2] if success
89
+
90
+ context.err(self, source, @choice_error, [value1, value2])
91
+ end
92
+
93
+ # Three-alternative fast path
94
+ def try_three(a1, a2, a3, source, context, consume_all)
95
+ success, value1 = a1.apply(source, context, consume_all)
96
+ return [success, value1] if success
97
+
98
+ success, value2 = a2.apply(source, context, consume_all)
99
+ return [success, value2] if success
100
+
101
+ success, value3 = a3.apply(source, context, consume_all)
102
+ return [success, value3] if success
103
+
104
+ context.err(self, source, @choice_error, [value1, value2, value3])
105
+ end
106
+
107
+ # General case for N alternatives
108
+ def try_many(options, source, context, consume_all)
109
+ errors = nil
110
+
111
+ options.each do |alt|
112
+ success, value = alt.apply(source, context, consume_all)
113
+ return [success, value] if success
114
+
115
+ errors ||= []
116
+ errors << value
117
+ end
118
+
119
+ context.err(self, source, @choice_error, errors)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all parser atoms. Handles parsing orchestration,
4
+ # memoization, error handling, and result processing.
5
+ #
6
+ # Concrete atoms must implement #try(source, context, consume_all).
7
+ #
8
+ # @abstract Implement #try to create custom parser atoms
9
+ module Parsanol
10
+ module Atoms
11
+ class Base
12
+ include Parsanol::Atoms::Precedence
13
+ include Parsanol::Atoms::DSL
14
+ include Parsanol::Atoms::CanFlatten
15
+ include Parsanol::FirstSet
16
+
17
+ # Label used for error messages (optional)
18
+ attr_accessor :label
19
+
20
+ # Error message for unconsumed input
21
+ UNCONSUMED_INPUT_MSG = "Don't know what to do with "
22
+
23
+ # Primary parsing interface. Takes a string or Source and returns
24
+ # the parsed tree, or raises ParseFailed on error.
25
+ #
26
+ # @param source [String, Parsanol::Source] input to parse
27
+ # @param options [Hash] parsing options
28
+ # @option options [Parsanol::ErrorReporter] :reporter error collector
29
+ # @option options [Boolean] :prefix allow partial parse (default: false)
30
+ # @return [Object] the parsed result
31
+ # @raise [Parsanol::ParseFailed] on parse failure
32
+ def parse(source, options = {})
33
+ input = normalize_input(source)
34
+ must_consume_all = !options[:prefix]
35
+
36
+ # Initial parse attempt (no error collection)
37
+ success, value = run_with_context(input, nil, must_consume_all)
38
+ return finalize_result(value) if success
39
+
40
+ # Reparse with error reporting for diagnostics
41
+ report_detailed_error(input, must_consume_all, options[:reporter],
42
+ value)
43
+ end
44
+
45
+ # Creates a new parsing context and executes the atom.
46
+ #
47
+ # @param input [Parsanol::Source] the source
48
+ # @param reporter [Object, nil] error reporter
49
+ # @param consume_all [Boolean] require complete consumption
50
+ # @return [Array(Boolean, Object)] outcome tuple
51
+ def run_with_context(input, reporter, consume_all)
52
+ parser_class = detect_parser_class
53
+ context = Parsanol::Atoms::Context.new(reporter,
54
+ parser_class: parser_class)
55
+ apply(input, context, consume_all)
56
+ end
57
+
58
+ # Core execution method. Manages position, caching, and error handling.
59
+ #
60
+ # @param input [Parsanol::Source] source to parse
61
+ # @param context [Parsanol::Atoms::Context] parsing state
62
+ # @param consume_all [Boolean] consume entire input
63
+ # @return [Array(Boolean, Object)] outcome pair
64
+ def apply(input, context, consume_all = false)
65
+ position_before = input.bytepos
66
+ outcome = context.try_with_cache(self, input, consume_all)
67
+ succeeded = outcome.first
68
+
69
+ return handle_failure(input, position_before, outcome) unless succeeded
70
+
71
+ context.succ(input)
72
+
73
+ # Verify full consumption when required
74
+ if consume_all && input.chars_left.positive?
75
+ return unconsumed_error(input, context,
76
+ position_before)
77
+ end
78
+
79
+ outcome
80
+ end
81
+
82
+ # Abstract matching method - override in subclasses.
83
+ #
84
+ # @param input [Parsanol::Source] source
85
+ # @param context [Parsanol::Atoms::Context] context
86
+ # @param consume_all [Boolean] consume all flag
87
+ # @return [Array(Boolean, Object)] parse result
88
+ # @raise [NotImplementedError] if not overridden
89
+ def try(input, context, consume_all)
90
+ raise NotImplementedError,
91
+ "Atom must implement #try(source, context, consume_all)"
92
+ end
93
+
94
+ # Whether packrat caching benefits this atom.
95
+ # Override to disable caching for simple atoms.
96
+ #
97
+ # @return [Boolean]
98
+ def cached?
99
+ true
100
+ end
101
+
102
+ # Whether this atom produces flat results.
103
+ # When true, flattening can be skipped.
104
+ #
105
+ # @return [Boolean]
106
+ def flat?
107
+ false
108
+ end
109
+
110
+ # DSL for setting precedence level (for pretty-printing).
111
+ #
112
+ # @param level [Integer] precedence value
113
+ def self.precedence(level)
114
+ define_method(:precedence) { level }
115
+ end
116
+ precedence ATOM
117
+
118
+ # String representation with precedence-aware parenthesization.
119
+ #
120
+ # @param outer [Integer] caller's precedence
121
+ # @return [String]
122
+ def to_s(outer = TOP)
123
+ text = label || to_s_inner(precedence)
124
+ outer < precedence ? "(#{text})" : text
125
+ end
126
+
127
+ def inspect
128
+ to_s(TOP)
129
+ end
130
+
131
+ protected
132
+
133
+ # Pre-allocated constant result tuples
134
+ NIL_OK = [true, nil].freeze
135
+ EMPTY_ARR = [].freeze
136
+ REP_TAG = [:repetition].freeze
137
+ REP_OK = [true, REP_TAG].freeze
138
+ SEQ_TAG = [:sequence].freeze
139
+ SEQ_OK = [true, SEQ_TAG].freeze
140
+ EMPTY_MAP = {}.freeze
141
+ MAP_OK = [true, EMPTY_MAP].freeze
142
+ CAP_TAG = [:capture].freeze
143
+ CAP_OK = [true, CAP_TAG].freeze
144
+
145
+ # Creates a success tuple.
146
+ #
147
+ # @param data [Object] the value
148
+ # @return [Array(true, Object)]
149
+ def ok(data)
150
+ return NIL_OK if data.nil?
151
+ return [true, EMPTY_ARR] if data.equal?(EMPTY_ARR)
152
+ return MAP_OK if data.equal?(EMPTY_MAP)
153
+ return REP_OK if data.equal?(REP_TAG)
154
+ return SEQ_OK if data.equal?(SEQ_TAG)
155
+ return CAP_OK if data.equal?(CAP_TAG)
156
+
157
+ [true, data]
158
+ end
159
+
160
+ # Alias for ok (legacy compatibility)
161
+ alias succ ok
162
+
163
+ private
164
+
165
+ # Converts raw input to Source if needed.
166
+ def normalize_input(source)
167
+ source.respond_to?(:line_and_column) ? source : Parsanol::Source.new(source)
168
+ end
169
+
170
+ # Detects if we're in a Parser context.
171
+ def detect_parser_class
172
+ is_a?(Parsanol::Parser) ? self.class : nil
173
+ end
174
+
175
+ # Handles parse failure by restoring position.
176
+ def handle_failure(input, saved_pos, outcome)
177
+ input.bytepos = saved_pos
178
+ outcome
179
+ end
180
+
181
+ # Creates error for unconsumed input.
182
+ def unconsumed_error(input, context, saved_pos)
183
+ excess_pos = input.bytepos
184
+ preview = input.consume(10)
185
+ input.bytepos = saved_pos
186
+ context.err_at(self, input,
187
+ UNCONSUMED_INPUT_MSG + preview.to_s.inspect, excess_pos)
188
+ end
189
+
190
+ # Reports detailed error by reparsing with reporter.
191
+ def report_detailed_error(input, consume_all, reporter, _initial_error)
192
+ input.bytepos = 0
193
+ error_reporter = reporter || Parsanol::ErrorReporter::Tree.new
194
+ success, cause = run_with_context(input, error_reporter, consume_all)
195
+
196
+ # Second parse should also fail
197
+ raise "Invariant violation: parse succeeded during error reporting" if success
198
+
199
+ cause.raise
200
+ end
201
+
202
+ # Finalizes result by flattening.
203
+ def finalize_result(value)
204
+ flatten(value)
205
+ end
206
+ end
207
+ end
208
+ end