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,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Parsanol::Parslet - Nested compatibility layer for original Parslet API
|
|
4
|
+
#
|
|
5
|
+
# This provides backwards compatibility for code that uses the original Parslet API.
|
|
6
|
+
# Instead of root-level Parslet constant, we use Parsanol::Parslet as a nested module.
|
|
7
|
+
#
|
|
8
|
+
# == Supported Features
|
|
9
|
+
#
|
|
10
|
+
# - All parser atoms (str, match, any, sequence, alternative, repetition, etc.)
|
|
11
|
+
# - Parser class with rule definitions
|
|
12
|
+
# - Transform for AST construction
|
|
13
|
+
# - Error reporting with Cause
|
|
14
|
+
# - Treetop-style expression parsing via exp()
|
|
15
|
+
#
|
|
16
|
+
# == Limitations
|
|
17
|
+
#
|
|
18
|
+
# - Some advanced features may require direct Parsanol usage
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# require 'parsanol/parslet'
|
|
22
|
+
#
|
|
23
|
+
# class MyParser < Parsanol::Parslet::Parser
|
|
24
|
+
# include Parsanol::Parslet
|
|
25
|
+
# rule(:foo) { str('foo') }
|
|
26
|
+
# root(:foo)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# Migration from original Parslet:
|
|
30
|
+
# Before: require 'parslet'
|
|
31
|
+
# class MyParser < Parslet::Parser
|
|
32
|
+
# include Parslet
|
|
33
|
+
#
|
|
34
|
+
# After: require 'parsanol/parslet'
|
|
35
|
+
# class MyParser < Parsanol::Parslet::Parser
|
|
36
|
+
# include Parsanol::Parslet
|
|
37
|
+
|
|
38
|
+
require "parsanol"
|
|
39
|
+
|
|
40
|
+
module Parsanol
|
|
41
|
+
module Parslet
|
|
42
|
+
# Include Parsanol to get all DSL methods (str, match, any, etc.)
|
|
43
|
+
include Parsanol
|
|
44
|
+
|
|
45
|
+
# Error class alias for compatibility
|
|
46
|
+
ParseFailed = Parsanol::ParseFailed
|
|
47
|
+
|
|
48
|
+
# Atoms namespace - aliases to Parsanol atoms
|
|
49
|
+
# These are the atoms explicitly loaded by lib/parsanol/atoms.rb
|
|
50
|
+
module Atoms
|
|
51
|
+
Base = ::Parsanol::Atoms::Base
|
|
52
|
+
Str = ::Parsanol::Atoms::Str
|
|
53
|
+
Re = ::Parsanol::Atoms::Re
|
|
54
|
+
Sequence = ::Parsanol::Atoms::Sequence
|
|
55
|
+
Alternative = ::Parsanol::Atoms::Alternative
|
|
56
|
+
Repetition = ::Parsanol::Atoms::Repetition
|
|
57
|
+
Named = ::Parsanol::Atoms::Named
|
|
58
|
+
Entity = ::Parsanol::Atoms::Entity
|
|
59
|
+
Lookahead = ::Parsanol::Atoms::Lookahead
|
|
60
|
+
Cut = ::Parsanol::Atoms::Cut
|
|
61
|
+
Capture = ::Parsanol::Atoms::Capture
|
|
62
|
+
Scope = ::Parsanol::Atoms::Scope
|
|
63
|
+
Dynamic = ::Parsanol::Atoms::Dynamic
|
|
64
|
+
Infix = ::Parsanol::Atoms::Infix
|
|
65
|
+
Ignored = ::Parsanol::Atoms::Ignored
|
|
66
|
+
ParseFailed = ::Parsanol::ParseFailed
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Class aliases
|
|
70
|
+
Parser = ::Parsanol::Parser
|
|
71
|
+
Transform = ::Parsanol::Transform
|
|
72
|
+
Cause = ::Parsanol::Cause
|
|
73
|
+
Slice = ::Parsanol::Slice
|
|
74
|
+
Source = ::Parsanol::Source
|
|
75
|
+
Pattern = ::Parsanol::Pattern
|
|
76
|
+
Context = ::Parsanol::Context
|
|
77
|
+
|
|
78
|
+
# Module functions for DSL (delegate to Parsanol)
|
|
79
|
+
module_function
|
|
80
|
+
|
|
81
|
+
def match(str = nil)
|
|
82
|
+
Parsanol.match(str)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def str(str)
|
|
86
|
+
Parsanol.str(str)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def any
|
|
90
|
+
Parsanol.any
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def scope(&)
|
|
94
|
+
Parsanol.scope(&)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def dynamic(&)
|
|
98
|
+
Parsanol.dynamic(&)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def infix_expression(element, *operations, &)
|
|
102
|
+
Parsanol.infix_expression(element, *operations, &)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Parses a treetop-style expression string and returns the corresponding atom.
|
|
106
|
+
# Delegates to Parsanol.exp.
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# # the same as str('a') >> str('b').maybe
|
|
110
|
+
# exp(%q("a" "b"?))
|
|
111
|
+
#
|
|
112
|
+
# @param str [String] a treetop expression
|
|
113
|
+
# @return [Parsanol::Atoms::Base] the corresponding parser atom
|
|
114
|
+
def exp(str)
|
|
115
|
+
Parsanol.exp(str)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def sequence(symbol)
|
|
119
|
+
Parsanol.sequence(symbol)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def simple(symbol)
|
|
123
|
+
Parsanol.simple(symbol)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def subtree(symbol)
|
|
127
|
+
Parsanol.subtree(symbol)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Class method extensions for Parser
|
|
131
|
+
module ClassMethods
|
|
132
|
+
# Enable automatic rule optimization for all rules in this parser.
|
|
133
|
+
# @param enable [Boolean] whether to enable optimization
|
|
134
|
+
def optimize_rules!(enable = true)
|
|
135
|
+
@optimize_rules = enable
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Check if rule optimization is enabled.
|
|
139
|
+
# @return [Boolean]
|
|
140
|
+
def optimize_rules?
|
|
141
|
+
@optimize_rules = false if @optimize_rules.nil?
|
|
142
|
+
@optimize_rules
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Extend with class methods when included
|
|
147
|
+
def self.included(base)
|
|
148
|
+
base.extend(ClassMethods)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pattern binding classes for transform pattern matching.
|
|
4
|
+
# These classes represent placeholders in transform patterns that capture
|
|
5
|
+
# values during pattern matching.
|
|
6
|
+
#
|
|
7
|
+
# Inspired by Parslet (MIT License).
|
|
8
|
+
|
|
9
|
+
# Base class for all pattern bindings. Matches any subtree regardless of type.
|
|
10
|
+
# Used internally by Parsanol::Transform for pattern-based tree transformation.
|
|
11
|
+
module Parsanol
|
|
12
|
+
class Pattern
|
|
13
|
+
SubtreeBind = Struct.new(:symbol) do
|
|
14
|
+
# Returns the symbol that will be bound during matching.
|
|
15
|
+
#
|
|
16
|
+
# @return [Symbol] the binding variable name
|
|
17
|
+
def variable_name
|
|
18
|
+
symbol
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Human-readable representation of this binding.
|
|
22
|
+
#
|
|
23
|
+
# @return [String] description of the binding
|
|
24
|
+
def inspect
|
|
25
|
+
"#{binding_category}(#{symbol.inspect})"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Determines if this binding can match the given subtree.
|
|
29
|
+
# SubtreeBind is the most permissive - matches anything.
|
|
30
|
+
#
|
|
31
|
+
# @param subtree [Object] the value to test
|
|
32
|
+
# @return [true] always returns true
|
|
33
|
+
def can_bind?(_subtree)
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Extracts the binding category name from the class name.
|
|
40
|
+
#
|
|
41
|
+
# @return [String] lowercase category name
|
|
42
|
+
def binding_category
|
|
43
|
+
class_match = self.class.name.match(/::(\w+)Bind\z/)
|
|
44
|
+
return class_match[1].downcase if class_match
|
|
45
|
+
|
|
46
|
+
# Fallback for unexpected class names
|
|
47
|
+
"subtree"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Binding that matches only simple (leaf) values.
|
|
54
|
+
# Simple values are those that are neither Hash nor Array.
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# simple(:x) # matches strings, numbers, slices - but not hashes or arrays
|
|
58
|
+
module Parsanol
|
|
59
|
+
class Pattern
|
|
60
|
+
class SimpleBind < Parsanol::Pattern::SubtreeBind
|
|
61
|
+
# Tests if the subtree is a simple leaf value.
|
|
62
|
+
#
|
|
63
|
+
# @param subtree [Object] the value to test
|
|
64
|
+
# @return [Boolean] true if subtree is not a Hash or Array
|
|
65
|
+
def can_bind?(subtree)
|
|
66
|
+
!subtree.is_a?(Hash) && !subtree.is_a?(Array)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Binding that matches sequences of simple leaf values.
|
|
73
|
+
# A sequence is an Array where no element is a Hash or Array.
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# sequence(:items) # matches ['a', 'b', 'c'] but not ['a', {x: 1}]
|
|
77
|
+
module Parsanol
|
|
78
|
+
class Pattern
|
|
79
|
+
class SequenceBind < Parsanol::Pattern::SubtreeBind
|
|
80
|
+
# Tests if the subtree is a flat sequence of simple values.
|
|
81
|
+
#
|
|
82
|
+
# @param subtree [Object] the value to test
|
|
83
|
+
# @return [Boolean] true if subtree is an Array of simple values
|
|
84
|
+
def can_bind?(subtree)
|
|
85
|
+
return false unless subtree.is_a?(Array)
|
|
86
|
+
|
|
87
|
+
subtree.none? { |element| element.is_a?(Hash) || element.is_a?(Array) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pattern matching for parse tree structures.
|
|
4
|
+
#
|
|
5
|
+
# This class provides tree pattern matching functionality where patterns
|
|
6
|
+
# are expressed using hashes for key-value structures and arrays for
|
|
7
|
+
# sequences. Leaf nodes can be matched using binding expressions.
|
|
8
|
+
#
|
|
9
|
+
# @example Matching a function call tree
|
|
10
|
+
# tree = {
|
|
11
|
+
# function_call: {
|
|
12
|
+
# name: 'foobar',
|
|
13
|
+
# args: [1, 2, 3]
|
|
14
|
+
# }
|
|
15
|
+
# }
|
|
16
|
+
#
|
|
17
|
+
# pattern = Parsanol::Pattern.new(
|
|
18
|
+
# function_call: { name: simple(:name), args: sequence(:args) }
|
|
19
|
+
# )
|
|
20
|
+
# bindings = pattern.match(tree)
|
|
21
|
+
# # => { name: 'foobar', args: [1, 2, 3] }
|
|
22
|
+
#
|
|
23
|
+
# Note: Pattern matching is performed at a single subtree level only.
|
|
24
|
+
# For recursive matching throughout a tree, use Parsanol::Transform.
|
|
25
|
+
#
|
|
26
|
+
# Inspired by pattern matching concepts in functional programming.
|
|
27
|
+
#
|
|
28
|
+
module Parsanol
|
|
29
|
+
class Pattern
|
|
30
|
+
# Creates a new pattern matcher with the given pattern structure.
|
|
31
|
+
#
|
|
32
|
+
# @param pattern [Hash, Array, Object] the pattern to match against
|
|
33
|
+
def initialize(pattern)
|
|
34
|
+
@pattern_def = pattern
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Attempts to match the given subtree against this pattern.
|
|
38
|
+
#
|
|
39
|
+
# Returns a hash of variable bindings if matching succeeds, or nil if
|
|
40
|
+
# the pattern does not match. Existing bindings can be provided to
|
|
41
|
+
# verify consistency with previous matches.
|
|
42
|
+
#
|
|
43
|
+
# @param subtree [Object] the tree or value to match
|
|
44
|
+
# @param bindings [Hash, nil] existing variable bindings to verify
|
|
45
|
+
# @return [Hash, nil] bindings hash on success, nil on failure
|
|
46
|
+
#
|
|
47
|
+
# @example Matching with existing bindings
|
|
48
|
+
# pattern = Parsanol::Pattern.new('a')
|
|
49
|
+
# pattern.match('a', { foo: 'bar' })
|
|
50
|
+
# # => { foo: 'bar' }
|
|
51
|
+
#
|
|
52
|
+
def match(subtree, bindings = nil)
|
|
53
|
+
current_bindings = bindings ? bindings.dup : {}
|
|
54
|
+
if check_match(subtree, @pattern_def,
|
|
55
|
+
current_bindings)
|
|
56
|
+
current_bindings
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Core matching dispatcher based on types.
|
|
63
|
+
# Routes to appropriate matching strategy based on tree and pattern types.
|
|
64
|
+
#
|
|
65
|
+
# @param target [Object] the value being matched
|
|
66
|
+
# @param pattern_val [Object] the pattern to match against
|
|
67
|
+
# @param captured [Hash] accumulated bindings (modified in place)
|
|
68
|
+
# @return [Boolean] true if match succeeds
|
|
69
|
+
#
|
|
70
|
+
def check_match(target, pattern_val, captured)
|
|
71
|
+
if target.is_a?(Hash) && pattern_val.is_a?(Hash)
|
|
72
|
+
match_hash_structure(target, pattern_val, captured)
|
|
73
|
+
elsif target.is_a?(Array) && pattern_val.is_a?(Array)
|
|
74
|
+
match_array_elements(target, pattern_val, captured)
|
|
75
|
+
else
|
|
76
|
+
match_leaf_value(target, pattern_val, captured)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Matches leaf values (non-containers).
|
|
81
|
+
# Handles direct equality, case equality, and binding capture.
|
|
82
|
+
#
|
|
83
|
+
# @param target [Object] the value being matched
|
|
84
|
+
# @param pattern_val [Object] the pattern element
|
|
85
|
+
# @param captured [Hash] bindings hash
|
|
86
|
+
# @return [Boolean] true if match succeeds
|
|
87
|
+
#
|
|
88
|
+
def match_leaf_value(target, pattern_val, captured)
|
|
89
|
+
# Case equality covers exact matches and class matches
|
|
90
|
+
return true if pattern_val === target
|
|
91
|
+
|
|
92
|
+
# Check if pattern is a binding expression (like simple(:x))
|
|
93
|
+
if pattern_val.respond_to?(:can_bind?) && pattern_val.can_bind?(target)
|
|
94
|
+
return capture_binding(target, pattern_val, captured)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# No match possible
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Handles binding capture for expressions like simple(:name).
|
|
102
|
+
# If the variable is already bound, verifies consistency.
|
|
103
|
+
# Otherwise, creates a new binding.
|
|
104
|
+
#
|
|
105
|
+
# @param value [Object] the value to bind
|
|
106
|
+
# @param binder [Object] the binding expression object
|
|
107
|
+
# @param captured [Hash] bindings hash (modified in place)
|
|
108
|
+
# @return [Boolean] true if binding succeeds
|
|
109
|
+
#
|
|
110
|
+
def capture_binding(value, binder, captured)
|
|
111
|
+
var_key = binder.variable_name
|
|
112
|
+
|
|
113
|
+
# Verify existing binding consistency if present
|
|
114
|
+
return captured[var_key] == value if var_key && captured.key?(var_key)
|
|
115
|
+
|
|
116
|
+
# Store new binding
|
|
117
|
+
captured[var_key] = value if var_key
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Matches array structures element-by-element.
|
|
122
|
+
# Arrays must have identical length and each element must match.
|
|
123
|
+
#
|
|
124
|
+
# @param target_ary [Array] the array being matched
|
|
125
|
+
# @param pattern_ary [Array] the pattern array
|
|
126
|
+
# @param captured [Hash] bindings hash
|
|
127
|
+
# @return [Boolean] true if all elements match
|
|
128
|
+
#
|
|
129
|
+
def match_array_elements(target_ary, pattern_ary, captured)
|
|
130
|
+
# Length mismatch means no match
|
|
131
|
+
return false unless target_ary.length == pattern_ary.length
|
|
132
|
+
|
|
133
|
+
# Each position must match
|
|
134
|
+
target_ary.zip(pattern_ary).all? do |elem, pat|
|
|
135
|
+
check_match(elem, pat, captured)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Matches hash structures key-by-key.
|
|
140
|
+
# All keys in pattern must exist in target with matching values.
|
|
141
|
+
#
|
|
142
|
+
# @param target_hash [Hash] the hash being matched
|
|
143
|
+
# @param pattern_hash [Hash] the pattern hash
|
|
144
|
+
# @param captured [Hash] bindings hash
|
|
145
|
+
# @return [Boolean] true if all key-value pairs match
|
|
146
|
+
#
|
|
147
|
+
def match_hash_structure(target_hash, pattern_hash, captured)
|
|
148
|
+
# Size mismatch means no match
|
|
149
|
+
return false unless target_hash.size == pattern_hash.size
|
|
150
|
+
|
|
151
|
+
# Verify each expected key exists with matching value
|
|
152
|
+
pattern_hash.each do |key, expected|
|
|
153
|
+
return false unless target_hash.key?(key)
|
|
154
|
+
|
|
155
|
+
actual = target_hash[key]
|
|
156
|
+
return false unless check_match(actual, expected, captured)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
true
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Generic object pool for reducing garbage collection pressure.
|
|
5
|
+
#
|
|
6
|
+
# The ObjectPool class implements a simple object pooling strategy:
|
|
7
|
+
# - Objects are pre-allocated on initialization
|
|
8
|
+
# - Objects are reused instead of created new
|
|
9
|
+
# - Objects are reset before being returned to the pool
|
|
10
|
+
# - Pool size is bounded to prevent unbounded growth
|
|
11
|
+
#
|
|
12
|
+
# This reduces GC pressure by reusing objects instead of constantly
|
|
13
|
+
# creating and destroying them, which is particularly beneficial for
|
|
14
|
+
# frequently allocated objects like Slice instances.
|
|
15
|
+
#
|
|
16
|
+
# == Thread Safety
|
|
17
|
+
#
|
|
18
|
+
# This implementation is NOT thread-safe. If thread safety is required,
|
|
19
|
+
# wrap pool operations in a mutex or use thread-local pools.
|
|
20
|
+
#
|
|
21
|
+
# == Usage Example
|
|
22
|
+
#
|
|
23
|
+
# # Create a pool for Slice objects
|
|
24
|
+
# pool = Parsanol::ObjectPool.new(Parsanol::Slice, size: 1000)
|
|
25
|
+
#
|
|
26
|
+
# # Acquire an object from the pool
|
|
27
|
+
# slice = pool.acquire
|
|
28
|
+
# slice.instance_variable_set(:@bytepos, 0)
|
|
29
|
+
# slice.instance_variable_set(:@str, "hello")
|
|
30
|
+
#
|
|
31
|
+
# # Use the slice...
|
|
32
|
+
#
|
|
33
|
+
# # Return it to the pool for reuse
|
|
34
|
+
# pool.release(slice)
|
|
35
|
+
#
|
|
36
|
+
# == Object Reset Protocol
|
|
37
|
+
#
|
|
38
|
+
# Objects returned to the pool will have their reset! method called
|
|
39
|
+
# if they respond to it. This allows objects to clean up their state
|
|
40
|
+
# before being reused. If reset! is not defined, the object is still
|
|
41
|
+
# pooled but without automatic cleanup.
|
|
42
|
+
#
|
|
43
|
+
class ObjectPool
|
|
44
|
+
# @return [Integer] Maximum number of objects to keep in the pool
|
|
45
|
+
attr_reader :size
|
|
46
|
+
|
|
47
|
+
# @return [Hash] Statistics about pool usage
|
|
48
|
+
attr_reader :stats
|
|
49
|
+
|
|
50
|
+
# Initialize a new object pool.
|
|
51
|
+
#
|
|
52
|
+
# @param klass [Class] The class of objects to pool
|
|
53
|
+
# @param size [Integer] Maximum number of objects to keep in pool (default: 1000)
|
|
54
|
+
# @param preallocate [Boolean] Whether to pre-allocate objects on initialization (default: true)
|
|
55
|
+
#
|
|
56
|
+
# @example Create a pool with default settings
|
|
57
|
+
# pool = ObjectPool.new(Array, size: 1000)
|
|
58
|
+
#
|
|
59
|
+
# @example Create a pool without pre-allocation
|
|
60
|
+
# pool = ObjectPool.new(Array, size: 1000, preallocate: false)
|
|
61
|
+
#
|
|
62
|
+
def initialize(klass, size: 1000, preallocate: true)
|
|
63
|
+
@klass = klass
|
|
64
|
+
@size = size
|
|
65
|
+
@available = []
|
|
66
|
+
@stats = {
|
|
67
|
+
created: 0,
|
|
68
|
+
reused: 0,
|
|
69
|
+
released: 0,
|
|
70
|
+
discarded: 0,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Pre-allocate objects for efficiency if requested
|
|
74
|
+
# This reduces allocation overhead during initial parsing
|
|
75
|
+
preallocate(size) if preallocate && can_preallocate?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Acquire an object from the pool.
|
|
79
|
+
#
|
|
80
|
+
# If the pool has available objects, one is returned (and considered "reused").
|
|
81
|
+
# If the pool is empty, a new object is created (and considered "created").
|
|
82
|
+
#
|
|
83
|
+
# @return [Object] An object instance from the pool or newly created
|
|
84
|
+
#
|
|
85
|
+
# @example Acquire from pool
|
|
86
|
+
# obj = pool.acquire
|
|
87
|
+
#
|
|
88
|
+
def acquire
|
|
89
|
+
if @available.empty?
|
|
90
|
+
@stats[:created] += 1
|
|
91
|
+
@klass.new
|
|
92
|
+
else
|
|
93
|
+
@stats[:reused] += 1
|
|
94
|
+
@available.pop
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Return an object to the pool for reuse.
|
|
99
|
+
#
|
|
100
|
+
# Before returning to the pool:
|
|
101
|
+
# 1. If object responds to reset!, that method is called to clean up state
|
|
102
|
+
# 2. If pool is at capacity, the object is discarded instead of pooled
|
|
103
|
+
#
|
|
104
|
+
# This ensures:
|
|
105
|
+
# - Objects are cleaned before reuse (no stale state)
|
|
106
|
+
# - Pool doesn't grow unbounded (respects size limit)
|
|
107
|
+
#
|
|
108
|
+
# @param obj [Object] The object to return to the pool
|
|
109
|
+
# @return [Boolean] true if object was returned to pool, false if discarded
|
|
110
|
+
#
|
|
111
|
+
# @example Return object to pool
|
|
112
|
+
# pool.release(obj)
|
|
113
|
+
#
|
|
114
|
+
def release(obj)
|
|
115
|
+
# Don't pool if we're at capacity - discard instead
|
|
116
|
+
if @available.size >= @size
|
|
117
|
+
@stats[:discarded] += 1
|
|
118
|
+
return false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Reset object state if it supports the protocol
|
|
122
|
+
obj.reset! if obj.respond_to?(:reset!)
|
|
123
|
+
|
|
124
|
+
@stats[:released] += 1
|
|
125
|
+
@available.push(obj)
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get current pool statistics.
|
|
130
|
+
#
|
|
131
|
+
# Statistics include:
|
|
132
|
+
# - size: Maximum pool capacity
|
|
133
|
+
# - available: Number of objects currently available in pool
|
|
134
|
+
# - created: Total number of new objects created
|
|
135
|
+
# - reused: Total number of times objects were reused from pool
|
|
136
|
+
# - released: Total number of objects returned to pool
|
|
137
|
+
# - discarded: Total number of objects discarded (pool was full)
|
|
138
|
+
# - utilization: Percentage of acquires that were reused (0-100)
|
|
139
|
+
#
|
|
140
|
+
# @return [Hash] Hash containing pool statistics
|
|
141
|
+
#
|
|
142
|
+
# @example Get statistics
|
|
143
|
+
# stats = pool.stats
|
|
144
|
+
# puts "Pool utilization: #{stats[:utilization]}%"
|
|
145
|
+
#
|
|
146
|
+
def statistics
|
|
147
|
+
total_acquires = @stats[:created] + @stats[:reused]
|
|
148
|
+
utilization = total_acquires.zero? ? 0.0 : (@stats[:reused].to_f / total_acquires * 100)
|
|
149
|
+
|
|
150
|
+
{
|
|
151
|
+
size: @size,
|
|
152
|
+
available: @available.size,
|
|
153
|
+
created: @stats[:created],
|
|
154
|
+
reused: @stats[:reused],
|
|
155
|
+
released: @stats[:released],
|
|
156
|
+
discarded: @stats[:discarded],
|
|
157
|
+
utilization: utilization.round(2),
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Clear all objects from the pool.
|
|
162
|
+
#
|
|
163
|
+
# This removes all pooled objects and resets statistics.
|
|
164
|
+
# Useful for testing or when you want to force fresh allocations.
|
|
165
|
+
#
|
|
166
|
+
# @return [void]
|
|
167
|
+
#
|
|
168
|
+
# @example Clear the pool
|
|
169
|
+
# pool.clear!
|
|
170
|
+
#
|
|
171
|
+
def clear!
|
|
172
|
+
@available.clear
|
|
173
|
+
@stats = {
|
|
174
|
+
created: 0,
|
|
175
|
+
reused: 0,
|
|
176
|
+
released: 0,
|
|
177
|
+
discarded: 0,
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
# Check if the pooled class can be pre-allocated.
|
|
184
|
+
#
|
|
185
|
+
# Some classes require arguments to initialize and cannot be
|
|
186
|
+
# pre-allocated without those arguments. This method checks if
|
|
187
|
+
# the class has a zero-arity initialize method.
|
|
188
|
+
#
|
|
189
|
+
# @return [Boolean] true if class can be instantiated without arguments
|
|
190
|
+
#
|
|
191
|
+
def can_preallocate?
|
|
192
|
+
# Check if the class can be instantiated without arguments
|
|
193
|
+
# This is a heuristic - we try to create one instance to test
|
|
194
|
+
|
|
195
|
+
@klass.new
|
|
196
|
+
true
|
|
197
|
+
rescue ArgumentError
|
|
198
|
+
# Class requires arguments, cannot pre-allocate
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Pre-allocate objects to fill the pool.
|
|
203
|
+
#
|
|
204
|
+
# This is called during initialization if preallocate: true is set.
|
|
205
|
+
# Pre-allocation reduces allocation overhead during initial parsing.
|
|
206
|
+
#
|
|
207
|
+
# @param count [Integer] Number of objects to pre-allocate
|
|
208
|
+
# @return [void]
|
|
209
|
+
#
|
|
210
|
+
def preallocate(count)
|
|
211
|
+
count.times do
|
|
212
|
+
@available.push(@klass.new)
|
|
213
|
+
end
|
|
214
|
+
# Adjust stats to reflect pre-allocation as "released" not "created"
|
|
215
|
+
# since these objects haven't been acquired yet
|
|
216
|
+
@stats[:released] = count
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module Pools
|
|
5
|
+
# Specialized object pool for Array instances.
|
|
6
|
+
#
|
|
7
|
+
# ArrayPool extends ObjectPool to provide array-specific behavior,
|
|
8
|
+
# particularly ensuring arrays are cleared before being returned to
|
|
9
|
+
# the pool for reuse.
|
|
10
|
+
#
|
|
11
|
+
# == Usage
|
|
12
|
+
#
|
|
13
|
+
# pool = Parsanol::Pools::ArrayPool.new(size: 1000)
|
|
14
|
+
#
|
|
15
|
+
# # Acquire an array
|
|
16
|
+
# array = pool.acquire
|
|
17
|
+
# array << 'item1'
|
|
18
|
+
# array << 'item2'
|
|
19
|
+
#
|
|
20
|
+
# # Return to pool (automatically cleared)
|
|
21
|
+
# pool.release(array)
|
|
22
|
+
#
|
|
23
|
+
# # Next acquire gets a clean, empty array
|
|
24
|
+
# array2 = pool.acquire
|
|
25
|
+
# array2.empty? # => true
|
|
26
|
+
#
|
|
27
|
+
# == Why Pool Arrays?
|
|
28
|
+
#
|
|
29
|
+
# Profiling (Session 19) showed that array allocations account for
|
|
30
|
+
# 74% of memory usage during parsing. Temporary arrays used for:
|
|
31
|
+
# - Collecting repetition results
|
|
32
|
+
# - Building sequence results
|
|
33
|
+
# - Accumulating alternative matches
|
|
34
|
+
#
|
|
35
|
+
# By pooling arrays, we can:
|
|
36
|
+
# - Reduce array allocations by 60-70%
|
|
37
|
+
# - Decrease memory pressure
|
|
38
|
+
# - Improve overall parsing performance
|
|
39
|
+
#
|
|
40
|
+
class ArrayPool < Parsanol::ObjectPool
|
|
41
|
+
# Initialize a new ArrayPool.
|
|
42
|
+
#
|
|
43
|
+
# @param size [Integer] Maximum number of Arrays to pool (default: 1000)
|
|
44
|
+
# @param preallocate [Boolean] Whether to pre-allocate arrays (default: true)
|
|
45
|
+
#
|
|
46
|
+
# @example Create an ArrayPool
|
|
47
|
+
# pool = ArrayPool.new(size: 2000)
|
|
48
|
+
#
|
|
49
|
+
def initialize(size: 1000, preallocate: true)
|
|
50
|
+
super(Array, size: size, preallocate: preallocate)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Return an array to the pool after clearing its contents.
|
|
54
|
+
#
|
|
55
|
+
# This override ensures arrays are always empty when returned to
|
|
56
|
+
# the pool, preventing stale data from polluting future uses.
|
|
57
|
+
#
|
|
58
|
+
# @param array [Array] The array to return to the pool
|
|
59
|
+
# @return [Boolean] true if returned to pool, false if discarded
|
|
60
|
+
#
|
|
61
|
+
# @example Release with automatic clearing
|
|
62
|
+
# array = pool.acquire
|
|
63
|
+
# array << 1 << 2 << 3
|
|
64
|
+
# pool.release(array)
|
|
65
|
+
# # Array is now cleared and back in pool
|
|
66
|
+
#
|
|
67
|
+
def release(array)
|
|
68
|
+
# Clear array before pooling to prevent stale data
|
|
69
|
+
# Note: Array#clear is more efficient than array = []
|
|
70
|
+
array.clear
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|