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
|
@@ -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
|