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,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Interval tree implementation for GPeg-style incremental parsing
4
+ # Based on the GPeg paper: "Fast Incremental PEG Parsing" (Yedidia, SLE 2021)
5
+ #
6
+ # This data structure stores memoization results keyed by position intervals [start, end)
7
+ # rather than single positions, enabling efficient invalidation of changed regions.
8
+ #
9
+ # Performance characteristics:
10
+ # - Insert: O(log n)
11
+ # - Query: O(log n + k) where k is number of overlapping intervals
12
+ # - Delete overlapping: O(log n + k)
13
+ #
14
+ module Parsanol
15
+ class IntervalTree
16
+ # A node in the interval tree
17
+ # Each node stores an interval [low, high) and associated data
18
+ class Node
19
+ attr_accessor :interval, :data, :max, :left, :right
20
+
21
+ def initialize(low, high, data)
22
+ @interval = [low, high] # [start, end) half-open interval
23
+ @data = data
24
+ @max = high # Maximum endpoint in subtree
25
+ @left = nil
26
+ @right = nil
27
+ end
28
+
29
+ def low
30
+ @interval[0]
31
+ end
32
+
33
+ def high
34
+ @interval[1]
35
+ end
36
+ end
37
+
38
+ def initialize
39
+ @root = nil
40
+ @size = 0
41
+ end
42
+
43
+ attr_reader :size
44
+
45
+ # Insert an interval with associated data
46
+ # @param low [Integer] Start position (inclusive)
47
+ # @param high [Integer] End position (exclusive)
48
+ # @param data [Object] Data to associate with this interval
49
+ def insert(low, high, data)
50
+ @root = insert_recursive(@root, low, high, data)
51
+ @size += 1
52
+ end
53
+
54
+ # Query for all intervals that overlap with [low, high)
55
+ # @param low [Integer] Start position (inclusive)
56
+ # @param high [Integer] End position (exclusive)
57
+ # @return [Array<Object>] Array of data from overlapping intervals
58
+ def query_overlapping(low, high)
59
+ # Empty intervals cannot overlap with anything
60
+ return [] if low >= high
61
+
62
+ results = []
63
+ query_recursive(@root, low, high, results)
64
+ results
65
+ end
66
+
67
+ # Query for exact interval match
68
+ # @param low [Integer] Start position (inclusive)
69
+ # @param high [Integer] End position (exclusive)
70
+ # @return [Object, nil] Data if exact match found, nil otherwise
71
+ def query_exact(low, high)
72
+ find_exact(@root, low, high)
73
+ end
74
+
75
+ # Delete all intervals that overlap with [low, high)
76
+ # Returns array of deleted data
77
+ # @param low [Integer] Start position (inclusive)
78
+ # @param high [Integer] End position (exclusive)
79
+ # @return [Array<Object>] Array of data from deleted intervals
80
+ def delete_overlapping(low, high)
81
+ deleted = []
82
+ @root = delete_overlapping_recursive(@root, low, high, deleted)
83
+ @size -= deleted.size
84
+ deleted
85
+ end
86
+
87
+ # Clear all intervals
88
+ def clear
89
+ @root = nil
90
+ @size = 0
91
+ end
92
+
93
+ # Check if tree is empty
94
+ def empty?
95
+ @root.nil?
96
+ end
97
+
98
+ private
99
+
100
+ # Insert node recursively maintaining BST property on interval start
101
+ def insert_recursive(node, low, high, data)
102
+ return Node.new(low, high, data) if node.nil?
103
+
104
+ # BST insertion based on interval start position
105
+ if low < node.low
106
+ node.left = insert_recursive(node.left, low, high, data)
107
+ else
108
+ node.right = insert_recursive(node.right, low, high, data)
109
+ end
110
+
111
+ # Update max endpoint in this subtree
112
+ node.max = [node.max, high].max
113
+ node.max = [node.max, node.left.max].max if node.left
114
+ node.max = [node.max, node.right.max].max if node.right
115
+
116
+ node
117
+ end
118
+
119
+ # Query recursively for overlapping intervals
120
+ def query_recursive(node, low, high, results)
121
+ return if node.nil?
122
+
123
+ # If no interval in this subtree can overlap, prune search
124
+ return if node.max <= low
125
+
126
+ # Check left subtree (may have overlapping intervals)
127
+ query_recursive(node.left, low, high, results) if node.left
128
+
129
+ # Check current node for overlap
130
+ # Two intervals [a,b) and [c,d) overlap if: a < d AND c < b
131
+ results << node.data if node.low < high && low < node.high
132
+
133
+ # Check right subtree
134
+ # Only search right if intervals starting there could overlap
135
+ query_recursive(node.right, low, high, results) if node.right && node.low < high
136
+ end
137
+
138
+ # Find exact interval match
139
+ def find_exact(node, low, high)
140
+ return nil if node.nil?
141
+
142
+ return node.data if node.low == low && node.high == high
143
+
144
+ # Search in appropriate subtree
145
+ if low < node.low
146
+ find_exact(node.left, low, high)
147
+ else
148
+ find_exact(node.right, low, high)
149
+ end
150
+ end
151
+
152
+ # Delete overlapping intervals recursively
153
+ def delete_overlapping_recursive(node, low, high, deleted)
154
+ return nil if node.nil?
155
+
156
+ # Recursively delete from left subtree
157
+ node.left = delete_overlapping_recursive(node.left, low, high, deleted) if node.left
158
+
159
+ # Recursively delete from right subtree
160
+ node.right = delete_overlapping_recursive(node.right, low, high, deleted) if node.right
161
+
162
+ # Check if current node overlaps
163
+ if node.low < high && low < node.high
164
+ # This node overlaps - delete it
165
+ deleted << node.data
166
+
167
+ # Remove this node and reinsert children
168
+ if node.left.nil?
169
+ return node.right
170
+ elsif node.right.nil?
171
+ return node.left
172
+ else
173
+ # Node has two children - replace with inorder successor
174
+ # Find minimum node in right subtree
175
+ min_node = find_min(node.right)
176
+
177
+ # Replace current node's interval and data with successor's
178
+ node.interval = min_node.interval
179
+ node.data = min_node.data
180
+
181
+ # Delete the successor from right subtree
182
+ node.right = delete_min(node.right)
183
+ end
184
+ end
185
+
186
+ # Update max for this node after potential deletions
187
+ if node
188
+ node.max = node.high
189
+ node.max = [node.max, node.left.max].max if node.left
190
+ node.max = [node.max, node.right.max].max if node.right
191
+ end
192
+
193
+ node
194
+ end
195
+
196
+ # Find minimum node in subtree (leftmost)
197
+ def find_min(node)
198
+ return node if node.left.nil?
199
+
200
+ find_min(node.left)
201
+ end
202
+
203
+ # Delete minimum node from subtree
204
+ def delete_min(node)
205
+ return node.right if node.left.nil?
206
+
207
+ node.left = delete_min(node.left)
208
+
209
+ # Update max
210
+ node.max = node.high
211
+ node.max = [node.max, node.left.max].max if node.left
212
+ node.max = [node.max, node.right.max].max if node.right
213
+
214
+ node
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ # Lazy wrapper around Buffer that defers array materialization.
5
+ #
6
+ # LazyResult wraps a Buffer and only creates an Array when the result
7
+ # is actually accessed. This reduces allocations for results that are
8
+ # never used (cache hits, backtracking, etc.).
9
+ #
10
+ # == Usage
11
+ #
12
+ # lazy = LazyResult.new(buffer, context)
13
+ # # No array allocated yet
14
+ #
15
+ # lazy.to_a # Now array is materialized and cached
16
+ # lazy.to_a # Returns cached array
17
+ #
18
+ # == Transparency
19
+ #
20
+ # LazyResult acts like an Array for most operations:
21
+ # - Enumerable methods work (each, map, select, etc.)
22
+ # - Array access works ([], size, empty?, etc.)
23
+ # - Can be used in transforms without changes
24
+ #
25
+ class LazyResult
26
+ # @return [Buffer] The underlying buffer
27
+ attr_reader :buffer
28
+
29
+ # @return [Context] The context (for buffer release)
30
+ attr_reader :context
31
+
32
+ # @return [Array, nil] Cached materialized array
33
+ attr_reader :materialized
34
+
35
+ # Initialize a new LazyResult.
36
+ #
37
+ # @param buffer [Buffer] Buffer containing elements
38
+ # @param context [Context] Context for buffer management
39
+ #
40
+ def initialize(buffer, context)
41
+ @buffer = buffer
42
+ @context = context
43
+ @materialized = nil
44
+ end
45
+
46
+ # Materialize to array (with caching).
47
+ #
48
+ # First call creates array from buffer, subsequent calls return cached.
49
+ #
50
+ # @return [Array] Materialized array
51
+ #
52
+ def to_a
53
+ @materialized ||= @buffer.to_a
54
+ end
55
+
56
+ # Get element at index (materializes if needed).
57
+ #
58
+ # @param index [Integer] Zero-based index
59
+ # @return [Object] Element at index
60
+ #
61
+ def [](index)
62
+ to_a[index]
63
+ end
64
+
65
+ # Get number of elements.
66
+ #
67
+ # @return [Integer] Number of elements
68
+ #
69
+ def size
70
+ @buffer.size
71
+ end
72
+
73
+ alias length size
74
+
75
+ # Check if empty.
76
+ #
77
+ # @return [Boolean] true if no elements
78
+ #
79
+ def empty?
80
+ @buffer.empty?
81
+ end
82
+
83
+ # Iterate over elements (materializes if needed).
84
+ #
85
+ # @yield [element] Each element
86
+ # @return [Enumerator, self] Enumerator if no block, self otherwise
87
+ #
88
+ def each(&block)
89
+ return to_enum(:each) unless block_given?
90
+
91
+ to_a.each(&block)
92
+ self
93
+ end
94
+
95
+ # Check if acts like an array.
96
+ #
97
+ # @param other [Class] Class to check against
98
+ # @return [Boolean] true if Array
99
+ #
100
+ def is_a?(other)
101
+ other == Array || super
102
+ end
103
+
104
+ alias kind_of? is_a?
105
+
106
+ # Respond to array methods.
107
+ #
108
+ # @param method [Symbol] Method name
109
+ # @param include_private [Boolean] Include private methods
110
+ # @return [Boolean] true if responds
111
+ #
112
+ def respond_to?(method, include_private = false)
113
+ super || to_a.respond_to?(method, include_private)
114
+ end
115
+
116
+ # Delegate unknown methods to materialized array.
117
+ #
118
+ # @param method [Symbol] Method name
119
+ # @param args [Array] Arguments
120
+ # @param block [Proc] Block if given
121
+ # @return [Object] Result of method call
122
+ #
123
+ def method_missing(method, ...)
124
+ if to_a.respond_to?(method)
125
+ to_a.public_send(method, ...)
126
+ else
127
+ super
128
+ end
129
+ end
130
+
131
+ # Support respond_to_missing? for proper method_missing implementation.
132
+ #
133
+ # @param method [Symbol] Method name
134
+ # @param include_private [Boolean] Include private methods
135
+ # @return [Boolean] true if method is supported
136
+ #
137
+ def respond_to_missing?(method, include_private = false)
138
+ to_a.respond_to?(method, include_private) || super
139
+ end
140
+
141
+ # Compare with another object.
142
+ # LazyResult compares equal to arrays with the same content.
143
+ #
144
+ # @param other [Object] Object to compare with
145
+ # @return [Boolean] true if equal
146
+ #
147
+ def ==(other)
148
+ if other.is_a?(Array)
149
+ to_a == other
150
+ elsif other.is_a?(LazyResult)
151
+ to_a == other.to_a
152
+ else
153
+ super
154
+ end
155
+ end
156
+
157
+ alias eql? ==
158
+
159
+ # Hash code based on materialized array.
160
+ #
161
+ # @return [Integer] Hash code
162
+ #
163
+ def hash
164
+ to_a.hash
165
+ end
166
+
167
+ # Inspect for debugging.
168
+ #
169
+ # @return [String] Inspection string
170
+ #
171
+ def inspect
172
+ if @materialized
173
+ "#<LazyResult:#{object_id} materialized=#{@materialized.inspect}>"
174
+ else
175
+ "#<LazyResult:#{object_id} buffer.size=#{@buffer.size}>"
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parsanol/native'
4
+
5
+ module Parsanol
6
+ # Generic lexer for fast tokenization
7
+ #
8
+ # Create a lexer by subclassing and defining tokens:
9
+ #
10
+ # class JsonLexer < Parsanol::Lexer
11
+ # token :string, /"[^"]*"/
12
+ # token :number, /-?[0-9]+(\.[0-9]+)?/
13
+ # token :true, /true/
14
+ # token :false, /false/
15
+ # token :null, /null/
16
+ # token :lbrace, /\{/
17
+ # token :rbrace, /\}/
18
+ # token :lbracket, /\[/
19
+ # token :rbracket, /\]/
20
+ # token :colon, /:/
21
+ # token :comma, /,/
22
+ #
23
+ # ignore /\s+/
24
+ # end
25
+ #
26
+ # lexer = JsonLexer.new
27
+ # tokens = lexer.tokenize('{"name": "test"}')
28
+ #
29
+ class Lexer
30
+ class << self
31
+ # Define a token pattern
32
+ #
33
+ # @param name [Symbol] Token type name
34
+ # @param pattern [Regexp] Pattern to match
35
+ # @param priority [Integer] Priority for conflict resolution (higher = preferred)
36
+ # @param block [Proc] Optional block to transform the matched value
37
+ def token(name, pattern, priority: 0, &block)
38
+ token_definitions << Definition.new(
39
+ name: name.to_s,
40
+ pattern: pattern.source,
41
+ priority: priority,
42
+ ignore: false,
43
+ transform: block
44
+ )
45
+ end
46
+
47
+ # Define patterns to ignore (e.g., whitespace, comments)
48
+ #
49
+ # @param pattern [Regexp] Pattern to ignore
50
+ def ignore(pattern)
51
+ token_definitions << Definition.new(
52
+ name: '__ignore__',
53
+ pattern: pattern.source,
54
+ priority: 0,
55
+ ignore: true,
56
+ transform: nil
57
+ )
58
+ end
59
+
60
+ # Define keywords (identifiers with higher priority)
61
+ #
62
+ # @param keywords [Array<Symbol>] Keyword names
63
+ # @param priority [Integer] Priority (default: 100)
64
+ def keyword(*keywords, priority: 100)
65
+ keywords.each do |kw|
66
+ token_definitions << Definition.new(
67
+ name: kw.to_s.upcase,
68
+ pattern: Regexp.new(Regexp.escape(kw.to_s), Regexp::IGNORECASE).source,
69
+ priority: priority,
70
+ ignore: false,
71
+ transform: nil
72
+ )
73
+ end
74
+ end
75
+
76
+ # Get token definitions for this lexer class
77
+ #
78
+ # @return [Array<Definition>] Token definitions
79
+ def token_definitions
80
+ @token_definitions ||= []
81
+ end
82
+
83
+ # Inherit token definitions from parent class
84
+ def inherited(subclass)
85
+ super
86
+ subclass.instance_variable_set(:@token_definitions, token_definitions.dup)
87
+ end
88
+ end
89
+
90
+ # Token definition
91
+ Definition = Struct.new(:name, :pattern, :priority, :ignore, :transform)
92
+
93
+ # Initialize the lexer
94
+ def initialize
95
+ @lexer_id = nil
96
+ @transforms = build_transforms
97
+ end
98
+
99
+ # Tokenize input string
100
+ #
101
+ # @param input [String] Input to tokenize
102
+ # @return [Array<Hash>] Array of tokens with type, value, and location
103
+ def tokenize(input)
104
+ ensure_lexer_created
105
+
106
+ tokens = Native.tokenize_with_lexer(@lexer_id, input)
107
+
108
+ # Apply any transforms
109
+ tokens.map do |token|
110
+ transform = @transforms[token['type']]
111
+ if transform
112
+ token = token.dup
113
+ token['value'] = transform.call(token['value'])
114
+ end
115
+ token
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def ensure_lexer_created
122
+ return if @lexer_id
123
+
124
+ definitions = self.class.token_definitions.map do |d|
125
+ {
126
+ 'name' => d.name,
127
+ 'pattern' => d.pattern,
128
+ 'priority' => d.priority,
129
+ 'ignore' => d.ignore
130
+ }
131
+ end
132
+
133
+ @lexer_id = Native.create_lexer(definitions)
134
+ end
135
+
136
+ def build_transforms
137
+ transforms = {}
138
+ self.class.token_definitions.each do |d|
139
+ transforms[d.name] = d.transform if d.transform && !d.ignore
140
+ end
141
+ transforms
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generates Mermaid diagram visualizations of parser grammars.
4
+ # Mermaid is widely supported by GitHub, GitLab, Notion, and many other tools.
5
+ #
6
+ # @example Generate Mermaid diagram
7
+ # parser = MyParser.new
8
+ # puts parser.to_mermaid
9
+ #
10
+ # @example Generate diagram for specific rule
11
+ # puts parser.mermaid_for_rule(:expression)
12
+ #
13
+ # Inspired by Parslet (MIT License).
14
+
15
+ module Parsanol
16
+ # Generates Mermaid diagram syntax from parser atoms.
17
+ class MermaidBuilder
18
+ def initialize
19
+ @lines = ['graph TD']
20
+ @node_counter = 0
21
+ @connections = []
22
+ @seen_rules = Set.new
23
+ end
24
+
25
+ # Entry point for parser visualization
26
+ def visit_parser(root_atom)
27
+ add_node('Parser', 'root')
28
+ traverse(root_atom, 'Parser')
29
+ finalize
30
+ end
31
+
32
+ # Handles named rules
33
+ def visit_entity(rule_name, rule_block)
34
+ return if @seen_rules.include?(rule_name)
35
+
36
+ @seen_rules << rule_name
37
+
38
+ node_id = add_node(rule_name.to_s.upcase, 'rule')
39
+ connect(current_parent, node_id)
40
+ traverse(rule_block.call, node_id)
41
+ end
42
+
43
+ # Pass through named captures
44
+ def visit_named(_label, atom)
45
+ traverse(atom, current_parent)
46
+ end
47
+
48
+ # Pass through repetition
49
+ def visit_repetition(_tag, _min, _max, atom)
50
+ traverse(atom, current_parent)
51
+ end
52
+
53
+ # Process alternatives
54
+ def visit_alternative(alternatives)
55
+ alternatives.each { |alt| traverse(alt, current_parent) }
56
+ end
57
+
58
+ # Process sequence
59
+ def visit_sequence(members)
60
+ members.each { |member| traverse(member, current_parent) }
61
+ end
62
+
63
+ # Pass through lookahead
64
+ def visit_lookahead(_positive, atom)
65
+ traverse(atom, current_parent)
66
+ end
67
+
68
+ # Leaf nodes
69
+ def visit_re(regexp)
70
+ add_node("match(#{regexp.inspect})", 'terminal', style: 'ellipse')
71
+ end
72
+
73
+ def visit_str(string)
74
+ add_node("'#{string}'", 'terminal', style: 'ellipse')
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :current_parent
80
+
81
+ def add_node(label, _shape_type = 'rect', _style = nil)
82
+ @node_counter += 1
83
+ node_id = "node_#{@node_counter}"
84
+ @lines << " #{node_id}[\"#{escape_mermaid(label)}\"]"
85
+ node_id
86
+ end
87
+
88
+ def connect(from_id, to_id)
89
+ @connections << [from_id, to_id]
90
+ end
91
+
92
+ def escape_mermaid(text)
93
+ text.gsub('"', "'").gsub('\n', '\\n')
94
+ end
95
+
96
+ def finalize
97
+ @connections.each do |from, to|
98
+ @lines << " #{from} --> #{to}"
99
+ end
100
+ @lines << ''
101
+ @lines.join("\n")
102
+ end
103
+
104
+ def traverse(atom, parent)
105
+ @current_parent = parent
106
+ atom.accept(self)
107
+ end
108
+ end
109
+
110
+ # Mixin module that adds Mermaid diagram generation to parsers
111
+ module MermaidDiagram
112
+ # Generates a Mermaid diagram of the parser.
113
+ #
114
+ # @return [String] Mermaid diagram source
115
+ def to_mermaid
116
+ builder = MermaidBuilder.new
117
+ new.accept(builder)
118
+ builder.output
119
+ end
120
+
121
+ # Generates Mermaid diagram for a specific rule.
122
+ #
123
+ # @param rule_name [Symbol] name of the rule
124
+ # @return [String] Mermaid diagram source
125
+ def mermaid_for_rule(rule_name)
126
+ builder = MermaidBuilder.new
127
+ rule_method = method(rule_name)
128
+ raise NotImplementedError, "Rule '#{rule_name}' not found" unless rule_method
129
+
130
+ rule_method.call.accept(builder)
131
+ builder.output
132
+ end
133
+ end
134
+
135
+ # Extend Parser with Mermaid diagram generation
136
+ class Parser
137
+ extend MermaidDiagram
138
+ end
139
+ end