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.
- checksums.yaml +7 -0
- data/HISTORY.txt +12 -0
- data/LICENSE +23 -0
- data/README.adoc +487 -0
- data/Rakefile +135 -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 +122 -0
- data/lib/parsanol/atoms/base.rb +202 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +334 -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 +162 -0
- data/lib/parsanol/atoms/lookahead.rb +82 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +259 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +194 -0
- data/lib/parsanol/atoms/str.rb +103 -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 +149 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +177 -0
- data/lib/parsanol/incremental_parser.rb +177 -0
- data/lib/parsanol/interval_tree.rb +217 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/lexer.rb +144 -0
- data/lib/parsanol/mermaid.rb +139 -0
- data/lib/parsanol/native/parser.rb +612 -0
- data/lib/parsanol/native/serializer.rb +248 -0
- data/lib/parsanol/native/transformer.rb +435 -0
- data/lib/parsanol/native/types.rb +42 -0
- data/lib/parsanol/native.rb +217 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +179 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +50 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/ruby_transform.rb +107 -0
- data/lib/parsanol/options/serialized.rb +94 -0
- data/lib/parsanol/options/zero_copy.rb +128 -0
- data/lib/parsanol/options.rb +20 -0
- data/lib/parsanol/parallel.rb +133 -0
- data/lib/parsanol/parser.rb +182 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +159 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +175 -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 +261 -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 +146 -0
- data/lib/parsanol/source/line_cache.rb +109 -0
- data/lib/parsanol/source.rb +180 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +195 -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 +280 -0
- data/parsanol-ruby.gemspec +67 -0
- 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
|