parsanol 1.3.13-arm-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/HISTORY.txt +98 -0
- data/LICENSE +23 -0
- data/README.adoc +905 -0
- data/Rakefile +141 -0
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +123 -0
- data/lib/parsanol/atoms/base.rb +208 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +357 -0
- data/lib/parsanol/atoms/context_optimized.rb +38 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +66 -0
- data/lib/parsanol/atoms/dsl.rb +96 -0
- data/lib/parsanol/atoms/dynamic.rb +39 -0
- data/lib/parsanol/atoms/entity.rb +75 -0
- data/lib/parsanol/atoms/ignored.rb +37 -0
- data/lib/parsanol/atoms/infix.rb +167 -0
- data/lib/parsanol/atoms/lookahead.rb +85 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +277 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +195 -0
- data/lib/parsanol/atoms/str.rb +109 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +46 -0
- data/lib/parsanol/buffer.rb +133 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +122 -0
- data/lib/parsanol/context.rb +39 -0
- data/lib/parsanol/convenience.rb +36 -0
- data/lib/parsanol/edit_tracker.rb +111 -0
- data/lib/parsanol/error_reporter/contextual.rb +99 -0
- data/lib/parsanol/error_reporter/deepest.rb +120 -0
- data/lib/parsanol/error_reporter/tree.rb +63 -0
- data/lib/parsanol/error_reporter.rb +100 -0
- data/lib/parsanol/expression/treetop.rb +154 -0
- data/lib/parsanol/expression.rb +106 -0
- data/lib/parsanol/fast_mode.rb +187 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +179 -0
- data/lib/parsanol/incremental_parser.rb +182 -0
- data/lib/parsanol/interval_tree.rb +226 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/mermaid.rb +142 -0
- data/lib/parsanol/native/batch_decoder.rb +255 -0
- data/lib/parsanol/native/dynamic.rb +238 -0
- data/lib/parsanol/native/parser.rb +102 -0
- data/lib/parsanol/native/serializer.rb +252 -0
- data/lib/parsanol/native/transformer.rb +604 -0
- data/lib/parsanol/native/types.rb +29 -0
- data/lib/parsanol/native.rb +223 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/zero_copy.rb +127 -0
- data/lib/parsanol/options.rb +21 -0
- data/lib/parsanol/parallel.rb +128 -0
- data/lib/parsanol/parser.rb +242 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +162 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +182 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +94 -0
- data/lib/parsanol/resettable.rb +29 -0
- data/lib/parsanol/result.rb +46 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +266 -0
- data/lib/parsanol/rig/rspec.rb +71 -0
- data/lib/parsanol/rope.rb +81 -0
- data/lib/parsanol/scope.rb +104 -0
- data/lib/parsanol/slice.rb +160 -0
- data/lib/parsanol/source/line_cache.rb +102 -0
- data/lib/parsanol/source.rb +185 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +198 -0
- data/lib/parsanol/transform.rb +226 -0
- data/lib/parsanol/version.rb +5 -0
- data/lib/parsanol/wasm/README.md +80 -0
- data/lib/parsanol/wasm/package.json +51 -0
- data/lib/parsanol/wasm/parsanol.js +252 -0
- data/lib/parsanol/wasm/parslet.d.ts +129 -0
- data/lib/parsanol/wasm_parser.rb +240 -0
- data/lib/parsanol.rb +278 -0
- data/parsanol.gemspec +67 -0
- metadata +279 -0
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
|