parsanol 1.0.1-aarch64-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 (101) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +12 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +487 -0
  5. data/Rakefile +135 -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 +122 -0
  12. data/lib/parsanol/atoms/base.rb +202 -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 +334 -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 +162 -0
  24. data/lib/parsanol/atoms/lookahead.rb +82 -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 +259 -0
  28. data/lib/parsanol/atoms/scope.rb +35 -0
  29. data/lib/parsanol/atoms/sequence.rb +194 -0
  30. data/lib/parsanol/atoms/str.rb +103 -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 +149 -0
  46. data/lib/parsanol/first_set.rb +79 -0
  47. data/lib/parsanol/grammar_builder.rb +177 -0
  48. data/lib/parsanol/incremental_parser.rb +177 -0
  49. data/lib/parsanol/interval_tree.rb +217 -0
  50. data/lib/parsanol/lazy_result.rb +179 -0
  51. data/lib/parsanol/lexer.rb +144 -0
  52. data/lib/parsanol/mermaid.rb +139 -0
  53. data/lib/parsanol/native/parser.rb +612 -0
  54. data/lib/parsanol/native/serializer.rb +248 -0
  55. data/lib/parsanol/native/transformer.rb +435 -0
  56. data/lib/parsanol/native/types.rb +42 -0
  57. data/lib/parsanol/native.rb +217 -0
  58. data/lib/parsanol/optimizer.rb +85 -0
  59. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  60. data/lib/parsanol/optimizers/cut_inserter.rb +179 -0
  61. data/lib/parsanol/optimizers/lookahead_optimizer.rb +50 -0
  62. data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
  63. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  64. data/lib/parsanol/options/ruby_transform.rb +107 -0
  65. data/lib/parsanol/options/serialized.rb +94 -0
  66. data/lib/parsanol/options/zero_copy.rb +128 -0
  67. data/lib/parsanol/options.rb +20 -0
  68. data/lib/parsanol/parallel.rb +133 -0
  69. data/lib/parsanol/parser.rb +182 -0
  70. data/lib/parsanol/parslet.rb +151 -0
  71. data/lib/parsanol/pattern/binding.rb +91 -0
  72. data/lib/parsanol/pattern.rb +159 -0
  73. data/lib/parsanol/pool.rb +219 -0
  74. data/lib/parsanol/pools/array_pool.rb +75 -0
  75. data/lib/parsanol/pools/buffer_pool.rb +175 -0
  76. data/lib/parsanol/pools/position_pool.rb +92 -0
  77. data/lib/parsanol/pools/slice_pool.rb +64 -0
  78. data/lib/parsanol/position.rb +94 -0
  79. data/lib/parsanol/resettable.rb +29 -0
  80. data/lib/parsanol/result.rb +46 -0
  81. data/lib/parsanol/result_builder.rb +208 -0
  82. data/lib/parsanol/result_stream.rb +261 -0
  83. data/lib/parsanol/rig/rspec.rb +71 -0
  84. data/lib/parsanol/rope.rb +81 -0
  85. data/lib/parsanol/scope.rb +104 -0
  86. data/lib/parsanol/slice.rb +146 -0
  87. data/lib/parsanol/source/line_cache.rb +109 -0
  88. data/lib/parsanol/source.rb +180 -0
  89. data/lib/parsanol/source_location.rb +167 -0
  90. data/lib/parsanol/streaming_parser.rb +124 -0
  91. data/lib/parsanol/string_view.rb +195 -0
  92. data/lib/parsanol/transform.rb +226 -0
  93. data/lib/parsanol/version.rb +5 -0
  94. data/lib/parsanol/wasm/README.md +80 -0
  95. data/lib/parsanol/wasm/package.json +51 -0
  96. data/lib/parsanol/wasm/parsanol.js +252 -0
  97. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  98. data/lib/parsanol/wasm_parser.rb +240 -0
  99. data/lib/parsanol.rb +280 -0
  100. data/parsanol-ruby.gemspec +67 -0
  101. metadata +280 -0
@@ -0,0 +1,182 @@
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
+ # @param input [String] the string to parse
86
+ # @param mode_or_opts [Symbol, Hash] parsing mode or options hash
87
+ # @param kwargs [Hash] additional keyword options
88
+ #
89
+ # Modes:
90
+ # - :ruby - Pure Ruby parsing (default, always available)
91
+ # - :native - Use Rust extension if available, fallback to Ruby
92
+ # - :json - Return JSON string representation of parse tree
93
+ #
94
+ # Options:
95
+ # - :reporter - Custom error reporter instance
96
+ # - :prefix - Allow partial matching (default: false)
97
+ #
98
+ # @return [Hash, Array, String, Parsanol::Slice] parsed result
99
+ # @raise [Parsanol::ParseFailed] when parsing fails
100
+ #
101
+ def parse(input, mode_or_opts = {}, **kwargs)
102
+ if mode_or_opts.is_a?(Hash)
103
+ # Legacy API: parse(input, options={})
104
+ merged = mode_or_opts.merge(kwargs)
105
+ super(input, merged)
106
+ else
107
+ # New API: parse(input, mode:, **options)
108
+ dispatch_parse(mode_or_opts, input, kwargs)
109
+ end
110
+ end
111
+
112
+ # Parses multiple inputs in batch mode.
113
+ #
114
+ # @param inputs [Array<String>] strings to parse
115
+ # @param mode [Symbol] parsing mode (:ruby, :native, or :json)
116
+ # @param opts [Hash] additional options
117
+ # @return [Array] array of parse results
118
+ #
119
+ def parse_batch(inputs, mode: :ruby, **opts)
120
+ inputs.map { |str| parse(str, mode: mode, **opts) }
121
+ end
122
+
123
+ private
124
+
125
+ # Dispatches to the appropriate parsing backend based on mode.
126
+ #
127
+ # @param mode [Symbol] the parsing mode
128
+ # @param input [String] input to parse
129
+ # @param opts [Hash] parsing options
130
+ # @return [Object] parse result
131
+ #
132
+ def dispatch_parse(mode, input, opts)
133
+ case mode
134
+ when :ruby
135
+ parse_ruby(input, opts)
136
+ when :native
137
+ parse_native(input, opts)
138
+ when :json
139
+ parse_json(input, opts)
140
+ else
141
+ raise ArgumentError, "Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
142
+ end
143
+ end
144
+
145
+ # Pure Ruby parsing (delegates to Base implementation).
146
+ #
147
+ # @param input [String] input to parse
148
+ # @param opts [Hash] parsing options
149
+ # @return [Object] parse result
150
+ #
151
+
152
+ # Native extension parsing with Ruby fallback.
153
+ #
154
+ # @param input [String] input to parse
155
+ # @param opts [Hash] parsing options
156
+ # @return [Object] parse result
157
+ #
158
+ def parse_native(input, opts)
159
+ if Parsanol::Native.available?
160
+ Parsanol::Native.parse_parslet_compatible(root, input)
161
+ else
162
+ parse_ruby(input, opts)
163
+ end
164
+ end
165
+
166
+ # JSON output mode - returns JSON string representation.
167
+ #
168
+ # @param input [String] input to parse
169
+ # @param opts [Hash] parsing options
170
+ # @return [String] JSON representation of parse tree
171
+ #
172
+ def parse_json(input, opts)
173
+ if Parsanol::Native.available?
174
+ grammar_def = Parsanol::Native.serialize_grammar(root)
175
+ outcome = Parsanol::Native.parse(grammar_def, input)
176
+ outcome.to_json
177
+ else
178
+ parse_ruby(input, opts).to_json
179
+ end
180
+ end
181
+ end
182
+ end
@@ -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(&block)
94
+ Parsanol.scope(&block)
95
+ end
96
+
97
+ def dynamic(&block)
98
+ Parsanol.dynamic(&block)
99
+ end
100
+
101
+ def infix_expression(element, *operations, &reducer)
102
+ Parsanol.infix_expression(element, *operations, &reducer)
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,159 @@
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
+ check_match(subtree, @pattern_def, current_bindings) ? current_bindings : nil
55
+ end
56
+
57
+ private
58
+
59
+ # Core matching dispatcher based on types.
60
+ # Routes to appropriate matching strategy based on tree and pattern types.
61
+ #
62
+ # @param target [Object] the value being matched
63
+ # @param pattern_val [Object] the pattern to match against
64
+ # @param captured [Hash] accumulated bindings (modified in place)
65
+ # @return [Boolean] true if match succeeds
66
+ #
67
+ def check_match(target, pattern_val, captured)
68
+ if target.is_a?(Hash) && pattern_val.is_a?(Hash)
69
+ match_hash_structure(target, pattern_val, captured)
70
+ elsif target.is_a?(Array) && pattern_val.is_a?(Array)
71
+ match_array_elements(target, pattern_val, captured)
72
+ else
73
+ match_leaf_value(target, pattern_val, captured)
74
+ end
75
+ end
76
+
77
+ # Matches leaf values (non-containers).
78
+ # Handles direct equality, case equality, and binding capture.
79
+ #
80
+ # @param target [Object] the value being matched
81
+ # @param pattern_val [Object] the pattern element
82
+ # @param captured [Hash] bindings hash
83
+ # @return [Boolean] true if match succeeds
84
+ #
85
+ def match_leaf_value(target, pattern_val, captured)
86
+ # Case equality covers exact matches and class matches
87
+ return true if pattern_val === target
88
+
89
+ # Check if pattern is a binding expression (like simple(:x))
90
+ if pattern_val.respond_to?(:can_bind?) && pattern_val.can_bind?(target)
91
+ return capture_binding(target, pattern_val, captured)
92
+ end
93
+
94
+ # No match possible
95
+ false
96
+ end
97
+
98
+ # Handles binding capture for expressions like simple(:name).
99
+ # If the variable is already bound, verifies consistency.
100
+ # Otherwise, creates a new binding.
101
+ #
102
+ # @param value [Object] the value to bind
103
+ # @param binder [Object] the binding expression object
104
+ # @param captured [Hash] bindings hash (modified in place)
105
+ # @return [Boolean] true if binding succeeds
106
+ #
107
+ def capture_binding(value, binder, captured)
108
+ var_key = binder.variable_name
109
+
110
+ # Verify existing binding consistency if present
111
+ return captured[var_key] == value if var_key && captured.key?(var_key)
112
+
113
+ # Store new binding
114
+ captured[var_key] = value if var_key
115
+ true
116
+ end
117
+
118
+ # Matches array structures element-by-element.
119
+ # Arrays must have identical length and each element must match.
120
+ #
121
+ # @param target_ary [Array] the array being matched
122
+ # @param pattern_ary [Array] the pattern array
123
+ # @param captured [Hash] bindings hash
124
+ # @return [Boolean] true if all elements match
125
+ #
126
+ def match_array_elements(target_ary, pattern_ary, captured)
127
+ # Length mismatch means no match
128
+ return false unless target_ary.length == pattern_ary.length
129
+
130
+ # Each position must match
131
+ target_ary.zip(pattern_ary).all? do |elem, pat|
132
+ check_match(elem, pat, captured)
133
+ end
134
+ end
135
+
136
+ # Matches hash structures key-by-key.
137
+ # All keys in pattern must exist in target with matching values.
138
+ #
139
+ # @param target_hash [Hash] the hash being matched
140
+ # @param pattern_hash [Hash] the pattern hash
141
+ # @param captured [Hash] bindings hash
142
+ # @return [Boolean] true if all key-value pairs match
143
+ #
144
+ def match_hash_structure(target_hash, pattern_hash, captured)
145
+ # Size mismatch means no match
146
+ return false unless target_hash.size == pattern_hash.size
147
+
148
+ # Verify each expected key exists with matching value
149
+ pattern_hash.each do |key, expected|
150
+ return false unless target_hash.key?(key)
151
+
152
+ actual = target_hash[key]
153
+ return false unless check_match(actual, expected, captured)
154
+ end
155
+
156
+ true
157
+ end
158
+ end
159
+ end