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,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Parser atoms - the building blocks for grammars.
|
|
4
|
+
# Each atom type handles a specific parsing primitive or combinator.
|
|
5
|
+
module Parsanol
|
|
6
|
+
module Atoms
|
|
7
|
+
# Precedence levels for pretty-printing.
|
|
8
|
+
# Higher values bind more loosely.
|
|
9
|
+
module Precedence
|
|
10
|
+
ATOM = 1 # literals, entities
|
|
11
|
+
LOOKAHEAD = 2 # &expr, !expr
|
|
12
|
+
REPETITION = 3 # expr*, expr+, expr?
|
|
13
|
+
SEQUENCE = 4 # expr expr
|
|
14
|
+
CHOICE = 5 # expr | expr
|
|
15
|
+
TOP = 6 # outer level
|
|
16
|
+
|
|
17
|
+
# Backward-compatible aliases
|
|
18
|
+
BASE = ATOM
|
|
19
|
+
ALTERNATE = CHOICE
|
|
20
|
+
OUTER = TOP
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Load atom implementations
|
|
24
|
+
require 'parsanol/atoms/can_flatten'
|
|
25
|
+
require 'parsanol/atoms/context'
|
|
26
|
+
require 'parsanol/atoms/dsl'
|
|
27
|
+
require 'parsanol/atoms/base'
|
|
28
|
+
require 'parsanol/atoms/custom'
|
|
29
|
+
require 'parsanol/atoms/ignored'
|
|
30
|
+
require 'parsanol/atoms/named'
|
|
31
|
+
require 'parsanol/atoms/lookahead'
|
|
32
|
+
require 'parsanol/atoms/cut'
|
|
33
|
+
require 'parsanol/atoms/alternative'
|
|
34
|
+
require 'parsanol/atoms/sequence'
|
|
35
|
+
require 'parsanol/atoms/repetition'
|
|
36
|
+
require 'parsanol/atoms/re'
|
|
37
|
+
require 'parsanol/atoms/str'
|
|
38
|
+
require 'parsanol/atoms/entity'
|
|
39
|
+
require 'parsanol/atoms/capture'
|
|
40
|
+
require 'parsanol/atoms/dynamic'
|
|
41
|
+
require 'parsanol/atoms/scope'
|
|
42
|
+
require 'parsanol/atoms/infix'
|
|
43
|
+
# Load visitor pattern (must be after all atom classes)
|
|
44
|
+
require 'parsanol/atoms/visitor'
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# A fixed-size buffer for efficient array operations.
|
|
5
|
+
#
|
|
6
|
+
# Buffer wraps an array with a logical size separate from capacity.
|
|
7
|
+
# This allows reusing buffers without reallocating arrays, reducing GC pressure.
|
|
8
|
+
#
|
|
9
|
+
# == Usage
|
|
10
|
+
#
|
|
11
|
+
# buffer = Buffer.new(capacity: 10)
|
|
12
|
+
# buffer.push("a")
|
|
13
|
+
# buffer.push("b")
|
|
14
|
+
# buffer.size # => 2
|
|
15
|
+
# buffer.to_a # => ["a", "b"]
|
|
16
|
+
# buffer.clear!
|
|
17
|
+
# buffer.size # => 0 (but capacity still 10)
|
|
18
|
+
#
|
|
19
|
+
# == Size vs Capacity
|
|
20
|
+
#
|
|
21
|
+
# - Size: Number of elements logically in the buffer
|
|
22
|
+
# - Capacity: Maximum elements before reallocation
|
|
23
|
+
#
|
|
24
|
+
# Reusing buffers maintains capacity while resetting size.
|
|
25
|
+
#
|
|
26
|
+
class Buffer
|
|
27
|
+
include Resettable
|
|
28
|
+
|
|
29
|
+
# @return [Integer] Logical size (number of elements)
|
|
30
|
+
attr_reader :size
|
|
31
|
+
|
|
32
|
+
# @return [Integer] Maximum capacity before growth
|
|
33
|
+
attr_reader :capacity
|
|
34
|
+
|
|
35
|
+
# @return [Array] Underlying array storage
|
|
36
|
+
attr_reader :storage
|
|
37
|
+
|
|
38
|
+
# Initialize a new buffer with specified capacity.
|
|
39
|
+
#
|
|
40
|
+
# @param capacity [Integer] Initial capacity (default: 10)
|
|
41
|
+
#
|
|
42
|
+
def initialize(capacity: 10)
|
|
43
|
+
@capacity = capacity
|
|
44
|
+
@storage = Array.new(capacity)
|
|
45
|
+
@size = 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Add an element to the buffer.
|
|
49
|
+
#
|
|
50
|
+
# Grows buffer if needed, but this should be rare with proper size classes.
|
|
51
|
+
#
|
|
52
|
+
# @param element [Object] Element to add
|
|
53
|
+
# @return [self] For method chaining
|
|
54
|
+
#
|
|
55
|
+
def push(element)
|
|
56
|
+
grow! if @size >= @capacity
|
|
57
|
+
@storage[@size] = element
|
|
58
|
+
@size += 1
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
alias << push
|
|
63
|
+
|
|
64
|
+
# Get element at index.
|
|
65
|
+
#
|
|
66
|
+
# @param index [Integer] Zero-based index
|
|
67
|
+
# @return [Object] Element at index, or nil if out of bounds
|
|
68
|
+
#
|
|
69
|
+
def [](index)
|
|
70
|
+
return nil if index >= @size
|
|
71
|
+
|
|
72
|
+
@storage[index]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Set element at index.
|
|
76
|
+
#
|
|
77
|
+
# @param index [Integer] Zero-based index
|
|
78
|
+
# @param value [Object] Value to set
|
|
79
|
+
#
|
|
80
|
+
def []=(index, value)
|
|
81
|
+
@storage[index] = value if index < @size
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Convert buffer to array (creates new array slice of logical size).
|
|
85
|
+
#
|
|
86
|
+
# @return [Array] Array containing elements [0...size]
|
|
87
|
+
#
|
|
88
|
+
def to_a
|
|
89
|
+
@storage[0...@size]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Clear the buffer (reset logical size, keep capacity).
|
|
93
|
+
#
|
|
94
|
+
# @return [self] For method chaining
|
|
95
|
+
#
|
|
96
|
+
def clear!
|
|
97
|
+
# Clear references for GC (keep capacity)
|
|
98
|
+
@size.upto(@capacity - 1) { |i| @storage[i] = nil }
|
|
99
|
+
@size = 0
|
|
100
|
+
self
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check if buffer is empty.
|
|
104
|
+
#
|
|
105
|
+
# @return [Boolean] true if size is zero
|
|
106
|
+
#
|
|
107
|
+
def empty?
|
|
108
|
+
@size.zero?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Reset protocol for ObjectPool compatibility.
|
|
112
|
+
# Delegates to clear! for buffer cleanup.
|
|
113
|
+
#
|
|
114
|
+
# @return [self] For method chaining
|
|
115
|
+
#
|
|
116
|
+
def reset!
|
|
117
|
+
clear!
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# Grow buffer capacity (double it).
|
|
123
|
+
# Should be rare with proper size class selection.
|
|
124
|
+
#
|
|
125
|
+
def grow!
|
|
126
|
+
new_capacity = @capacity * 2
|
|
127
|
+
new_storage = Array.new(new_capacity)
|
|
128
|
+
@storage.each_with_index { |elem, i| new_storage[i] = elem }
|
|
129
|
+
@storage = new_storage
|
|
130
|
+
@capacity = new_capacity
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Module for streaming builder callbacks.
|
|
5
|
+
# Include this module in your builder class to receive callbacks
|
|
6
|
+
# during single-pass parsing with the streaming builder API.
|
|
7
|
+
#
|
|
8
|
+
# The streaming builder API allows maximum performance by eliminating
|
|
9
|
+
# intermediate AST construction. Instead, callbacks are invoked as
|
|
10
|
+
# parsing progresses, allowing you to construct custom output directly.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic string collector
|
|
13
|
+
# class StringCollector
|
|
14
|
+
# include Parsanol::BuilderCallbacks
|
|
15
|
+
#
|
|
16
|
+
# def initialize
|
|
17
|
+
# @strings = []
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# def on_string(value, offset, length)
|
|
21
|
+
# @strings << value
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# def finish
|
|
25
|
+
# @strings
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# grammar = Parsanol::Native.serialize_grammar(parser.root)
|
|
30
|
+
# builder = StringCollector.new
|
|
31
|
+
# result = Parsanol::Native.parse_with_builder(grammar, input, builder)
|
|
32
|
+
# # result: ["hello", "world"]
|
|
33
|
+
#
|
|
34
|
+
# @example Building a typed AST
|
|
35
|
+
# class AstBuilder
|
|
36
|
+
# include Parsanol::BuilderCallbacks
|
|
37
|
+
#
|
|
38
|
+
# def initialize
|
|
39
|
+
# @stack = []
|
|
40
|
+
# @current_hash = nil
|
|
41
|
+
# @current_key = nil
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# def on_hash_start(size = nil)
|
|
45
|
+
# @stack.push(@current_hash) if @current_hash
|
|
46
|
+
# @current_hash = {}
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# def on_hash_end(size)
|
|
50
|
+
# finished = @current_hash
|
|
51
|
+
# @current_hash = @stack.pop
|
|
52
|
+
# if @current_hash && @current_key
|
|
53
|
+
# @current_hash[@current_key] = finished
|
|
54
|
+
# @current_key = nil
|
|
55
|
+
# end
|
|
56
|
+
# finished
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# def on_hash_key(key)
|
|
60
|
+
# @current_key = key
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# def on_string(value, offset, length)
|
|
64
|
+
# if @current_hash && @current_key
|
|
65
|
+
# @current_hash[@current_key] = value
|
|
66
|
+
# @current_key = nil
|
|
67
|
+
# end
|
|
68
|
+
# end
|
|
69
|
+
#
|
|
70
|
+
# def finish
|
|
71
|
+
# @current_hash
|
|
72
|
+
# end
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
module BuilderCallbacks
|
|
76
|
+
# Called when parsing starts.
|
|
77
|
+
#
|
|
78
|
+
# @param input [String] The input being parsed
|
|
79
|
+
# @return [void]
|
|
80
|
+
def on_start(input); end
|
|
81
|
+
|
|
82
|
+
# Called when parsing succeeds.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
def on_success; end
|
|
86
|
+
|
|
87
|
+
# Called when parsing fails.
|
|
88
|
+
#
|
|
89
|
+
# @param message [String] The error message
|
|
90
|
+
# @return [void]
|
|
91
|
+
def on_error(message); end
|
|
92
|
+
|
|
93
|
+
# Called when a string value is matched.
|
|
94
|
+
#
|
|
95
|
+
# @param value [String] The matched string value
|
|
96
|
+
# @param offset [Integer] Byte offset in the original input
|
|
97
|
+
# @param length [Integer] Length of the matched string in bytes
|
|
98
|
+
# @return [void]
|
|
99
|
+
def on_string(value, offset, length); end
|
|
100
|
+
|
|
101
|
+
# Called when an integer value is matched.
|
|
102
|
+
#
|
|
103
|
+
# @param value [Integer] The matched integer value
|
|
104
|
+
# @return [void]
|
|
105
|
+
def on_int(value); end
|
|
106
|
+
|
|
107
|
+
# Called when a float value is matched.
|
|
108
|
+
#
|
|
109
|
+
# @param value [Float] The matched float value
|
|
110
|
+
# @return [void]
|
|
111
|
+
def on_float(value); end
|
|
112
|
+
|
|
113
|
+
# Called when a boolean value is matched.
|
|
114
|
+
#
|
|
115
|
+
# @param value [Boolean] The matched boolean value
|
|
116
|
+
# @return [void]
|
|
117
|
+
def on_bool(value); end
|
|
118
|
+
|
|
119
|
+
# Called when a nil/null value is matched.
|
|
120
|
+
#
|
|
121
|
+
# @return [void]
|
|
122
|
+
def on_nil; end
|
|
123
|
+
|
|
124
|
+
# Called when starting to parse a hash/object.
|
|
125
|
+
# Use this to initialize state for collecting key-value pairs.
|
|
126
|
+
#
|
|
127
|
+
# @param size [Integer, nil] Expected number of entries (may be nil)
|
|
128
|
+
# @return [void]
|
|
129
|
+
def on_hash_start(size = nil); end
|
|
130
|
+
|
|
131
|
+
# Called when finishing parsing a hash/object.
|
|
132
|
+
#
|
|
133
|
+
# @param size [Integer] Actual number of entries
|
|
134
|
+
# @return [void]
|
|
135
|
+
def on_hash_end(size); end
|
|
136
|
+
|
|
137
|
+
# Called when a hash key is encountered.
|
|
138
|
+
# The next value callback(s) will provide the value for this key.
|
|
139
|
+
#
|
|
140
|
+
# @param key [String] The hash key name
|
|
141
|
+
# @return [void]
|
|
142
|
+
def on_hash_key(key); end
|
|
143
|
+
|
|
144
|
+
# Called when a hash value is about to be parsed.
|
|
145
|
+
# Called after on_hash_key for the corresponding value.
|
|
146
|
+
#
|
|
147
|
+
# @param key [String] The hash key name
|
|
148
|
+
# @return [void]
|
|
149
|
+
def on_hash_value(key); end
|
|
150
|
+
|
|
151
|
+
# Called when starting to parse an array.
|
|
152
|
+
# Use this to initialize state for collecting array elements.
|
|
153
|
+
#
|
|
154
|
+
# @param size [Integer, nil] Expected number of elements (may be nil)
|
|
155
|
+
# @return [void]
|
|
156
|
+
def on_array_start(size = nil); end
|
|
157
|
+
|
|
158
|
+
# Called when an array element is about to be parsed.
|
|
159
|
+
#
|
|
160
|
+
# @param index [Integer] The index of the element
|
|
161
|
+
# @return [void]
|
|
162
|
+
def on_array_element(index); end
|
|
163
|
+
|
|
164
|
+
# Called when finishing parsing an array.
|
|
165
|
+
#
|
|
166
|
+
# @param size [Integer] Actual number of elements
|
|
167
|
+
# @return [void]
|
|
168
|
+
def on_array_end(size); end
|
|
169
|
+
|
|
170
|
+
# Called when starting to parse a named rule.
|
|
171
|
+
#
|
|
172
|
+
# @param name [String] The rule name
|
|
173
|
+
# @return [void]
|
|
174
|
+
def on_named_start(name); end
|
|
175
|
+
|
|
176
|
+
# Called when finishing parsing a named rule.
|
|
177
|
+
#
|
|
178
|
+
# @param name [String] The rule name
|
|
179
|
+
# @return [void]
|
|
180
|
+
def on_named_end(name); end
|
|
181
|
+
|
|
182
|
+
# Called when parsing is complete.
|
|
183
|
+
# Override this method to return your final constructed result.
|
|
184
|
+
#
|
|
185
|
+
# @return [Object] The final result of the builder
|
|
186
|
+
def finish; end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Built-in builders for common use cases
|
|
190
|
+
module Builders
|
|
191
|
+
# Debug builder that collects all events as strings.
|
|
192
|
+
# Useful for understanding the parsing flow.
|
|
193
|
+
class DebugBuilder
|
|
194
|
+
include BuilderCallbacks
|
|
195
|
+
|
|
196
|
+
attr_reader :events
|
|
197
|
+
|
|
198
|
+
def initialize
|
|
199
|
+
@events = []
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def on_start(input)
|
|
203
|
+
@events << "start: #{input.inspect}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def on_success
|
|
207
|
+
@events << 'success'
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def on_error(message)
|
|
211
|
+
@events << "error: #{message}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def on_string(value, offset, length)
|
|
215
|
+
@events << "string: #{value.inspect} @ #{offset}(#{length})"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def on_int(value)
|
|
219
|
+
@events << "int: #{value}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def on_float(value)
|
|
223
|
+
@events << "float: #{value}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def on_bool(value)
|
|
227
|
+
@events << "bool: #{value}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def on_nil
|
|
231
|
+
@events << 'nil'
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def on_hash_start(size = nil)
|
|
235
|
+
@events << "hash_start(#{size.inspect})"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def on_hash_end(size)
|
|
239
|
+
@events << "hash_end(#{size})"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def on_hash_key(key)
|
|
243
|
+
@events << "hash_key: #{key.inspect}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def on_hash_value(key)
|
|
247
|
+
@events << "hash_value: #{key.inspect}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def on_array_start(size = nil)
|
|
251
|
+
@events << "array_start(#{size.inspect})"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def on_array_element(index)
|
|
255
|
+
@events << "array_element[#{index}]"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def on_array_end(size)
|
|
259
|
+
@events << "array_end(#{size})"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def on_named_start(name)
|
|
263
|
+
@events << "named_start: #{name}"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def on_named_end(name)
|
|
267
|
+
@events << "named_end: #{name}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def finish
|
|
271
|
+
@events.join("\n")
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Builder that collects all string values.
|
|
276
|
+
class StringCollector
|
|
277
|
+
include BuilderCallbacks
|
|
278
|
+
|
|
279
|
+
attr_reader :strings
|
|
280
|
+
|
|
281
|
+
def initialize
|
|
282
|
+
@strings = []
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def on_start(input); end
|
|
286
|
+
|
|
287
|
+
def on_success; end
|
|
288
|
+
|
|
289
|
+
def on_error(message); end
|
|
290
|
+
|
|
291
|
+
def on_string(value, _offset, _length)
|
|
292
|
+
@strings << value
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def finish
|
|
296
|
+
@strings
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Builder that counts nodes by type.
|
|
301
|
+
class NodeCounter
|
|
302
|
+
include BuilderCallbacks
|
|
303
|
+
|
|
304
|
+
attr_reader :counts
|
|
305
|
+
|
|
306
|
+
def initialize
|
|
307
|
+
@counts = Hash.new(0)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def on_start(input); end
|
|
311
|
+
|
|
312
|
+
def on_success; end
|
|
313
|
+
|
|
314
|
+
def on_error(message); end
|
|
315
|
+
|
|
316
|
+
def on_string(_value, _offset, _length)
|
|
317
|
+
@counts[:string] += 1
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def on_int(_value)
|
|
321
|
+
@counts[:int] += 1
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def on_float(_value)
|
|
325
|
+
@counts[:float] += 1
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def on_bool(_value)
|
|
329
|
+
@counts[:bool] += 1
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def on_nil
|
|
333
|
+
@counts[:nil] += 1
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def on_hash_start(_size = nil)
|
|
337
|
+
@counts[:hash] += 1
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def on_array_start(_size = nil)
|
|
341
|
+
@counts[:array] += 1
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def on_named_start(_name)
|
|
345
|
+
@counts[:named] += 1
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def finish
|
|
349
|
+
@counts
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Represents a cause why a parse did fail. Stores information about
|
|
4
|
+
# parse failure including:
|
|
5
|
+
# - message: Human-readable error description
|
|
6
|
+
# - source: The source object being parsed
|
|
7
|
+
# - position: Byte position where error occurred
|
|
8
|
+
# - children: Nested causes (for deeper context)
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# cause = Parsanol::Cause.new(
|
|
12
|
+
# "Expected at least one",
|
|
13
|
+
# source,
|
|
14
|
+
# 5
|
|
15
|
+
# )
|
|
16
|
+
# cause.children # => []
|
|
17
|
+
# cause.to_s # => "Expected at least one"
|
|
18
|
+
#
|
|
19
|
+
module Parsanol
|
|
20
|
+
class Cause
|
|
21
|
+
# @return [Array<String>] Error message parts
|
|
22
|
+
attr_reader :message
|
|
23
|
+
|
|
24
|
+
# @return [Parsanol::Source] Source being parsed
|
|
25
|
+
attr_reader :source
|
|
26
|
+
|
|
27
|
+
# @return [Integer] Byte position where error occurred
|
|
28
|
+
attr_reader :position
|
|
29
|
+
|
|
30
|
+
# Alias for position (API compatibility)
|
|
31
|
+
alias pos position
|
|
32
|
+
|
|
33
|
+
# @return [Array<Cause>] Child causes
|
|
34
|
+
attr_reader :children
|
|
35
|
+
|
|
36
|
+
# Creates a new cause for parse failure
|
|
37
|
+
#
|
|
38
|
+
# @param message [String, Array<String>] Error description
|
|
39
|
+
# @param source [Parsanol::Source] Source being parsed
|
|
40
|
+
# @param position [Integer] Byte position where error occurred
|
|
41
|
+
# @param children [Array<Cause>] Nested causes (optional)
|
|
42
|
+
def initialize(message, source, position, children = [])
|
|
43
|
+
@message = Array(message)
|
|
44
|
+
@source = source
|
|
45
|
+
@position = position
|
|
46
|
+
@children = children.nil? ? [] : children
|
|
47
|
+
@parsing_label = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Factory method for creating a cause
|
|
51
|
+
#
|
|
52
|
+
# @param source [Parsanol::Source] source being parsed
|
|
53
|
+
# @param position [Integer] Byte position where error occurred
|
|
54
|
+
# @param message [String, Array<String>] Error description
|
|
55
|
+
# @param children [Array<Cause>] Nested causes
|
|
56
|
+
# @return [Cause] New cause instance
|
|
57
|
+
def self.format(source, position, message, children = [])
|
|
58
|
+
new(message, source, position, children)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Associates a label with this cause for parsing context
|
|
62
|
+
#
|
|
63
|
+
# @param label [String] Description of what was being parsed
|
|
64
|
+
# @return [void]
|
|
65
|
+
def set_label(label)
|
|
66
|
+
@parsing_label = " while parsing #{label}"
|
|
67
|
+
@children.each { |c| c.set_label(label) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Formats this cause as a human-readable string
|
|
71
|
+
#
|
|
72
|
+
# @return [String] Formatted error message
|
|
73
|
+
def to_s
|
|
74
|
+
line_num, col_num = @source.line_and_column(@position)
|
|
75
|
+
|
|
76
|
+
formatted_msg = @message.map do |msg|
|
|
77
|
+
msg.respond_to?(:to_slice) ? msg.content.inspect : msg.to_s
|
|
78
|
+
end.join
|
|
79
|
+
|
|
80
|
+
"#{formatted_msg} at line #{line_num} char #{col_num}#{@parsing_label}."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Generates a tree-style visualization of error causes
|
|
84
|
+
#
|
|
85
|
+
# @return [String] ASCII tree representation
|
|
86
|
+
def ascii_tree
|
|
87
|
+
output = StringIO.new
|
|
88
|
+
build_tree_recursive(self, output, [true])
|
|
89
|
+
output.string
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Raises a ParseFailed exception with this cause's information
|
|
93
|
+
#
|
|
94
|
+
# @raise [Parsanol::ParseFailed] Always
|
|
95
|
+
def raise
|
|
96
|
+
exc = Parsanol::ParseFailed.new(to_s, self)
|
|
97
|
+
Kernel.raise exc
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def build_tree_recursive(node, stream, prefix_flags)
|
|
103
|
+
render_prefix(stream, prefix_flags)
|
|
104
|
+
stream.puts node.to_s
|
|
105
|
+
|
|
106
|
+
node.children.each do |child|
|
|
107
|
+
is_last_child = (node.children.last == child)
|
|
108
|
+
build_tree_recursive(child, stream, prefix_flags + [is_last_child])
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def render_prefix(stream, prefix_flags)
|
|
113
|
+
return if prefix_flags.size < 2
|
|
114
|
+
|
|
115
|
+
prefix_flags[1..-2].each do |is_last|
|
|
116
|
+
stream.print is_last ? ' ' : '| '
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
stream.print prefix_flags.last ? '`- ' : '|- '
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Execution context for tree transformation rules.
|
|
4
|
+
# Provides a clean interface for accessing bound variables within transformation blocks.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# ctx = Parsanol::Context.new(name: 'Alice', value: 42)
|
|
8
|
+
# ctx.instance_eval { name } # => 'Alice'
|
|
9
|
+
# ctx.instance_eval { @value } # => 42
|
|
10
|
+
#
|
|
11
|
+
# Inspired by Parslet (MIT License).
|
|
12
|
+
module Parsanol
|
|
13
|
+
class Context
|
|
14
|
+
include Parsanol
|
|
15
|
+
|
|
16
|
+
# Creates a new context with the given bindings.
|
|
17
|
+
# Each binding becomes accessible as both a method and an instance variable.
|
|
18
|
+
#
|
|
19
|
+
# @param bindings [Hash] variable name => value pairs
|
|
20
|
+
def initialize(bindings)
|
|
21
|
+
bindings.each_pair do |key, val|
|
|
22
|
+
# Define accessor method on singleton class
|
|
23
|
+
define_singleton_method(key) { val }
|
|
24
|
+
# Also set as instance variable for @-style access
|
|
25
|
+
instance_variable_set("@#{key}", val)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Defines a method on the object's singleton class.
|
|
32
|
+
#
|
|
33
|
+
# @param name [Symbol, String] method name
|
|
34
|
+
# @yield block to execute when method is called
|
|
35
|
+
def define_singleton_method(name, &body)
|
|
36
|
+
singleton_class.define_method(name, &body)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|