parsanol 1.3.13-arm-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/HISTORY.txt +98 -0
- data/LICENSE +23 -0
- data/README.adoc +905 -0
- data/Rakefile +141 -0
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/ast_visitor.rb +122 -0
- data/lib/parsanol/atoms/alternative.rb +123 -0
- data/lib/parsanol/atoms/base.rb +208 -0
- data/lib/parsanol/atoms/can_flatten.rb +194 -0
- data/lib/parsanol/atoms/capture.rb +38 -0
- data/lib/parsanol/atoms/context.rb +357 -0
- data/lib/parsanol/atoms/context_optimized.rb +38 -0
- data/lib/parsanol/atoms/custom.rb +110 -0
- data/lib/parsanol/atoms/cut.rb +66 -0
- data/lib/parsanol/atoms/dsl.rb +96 -0
- data/lib/parsanol/atoms/dynamic.rb +39 -0
- data/lib/parsanol/atoms/entity.rb +75 -0
- data/lib/parsanol/atoms/ignored.rb +37 -0
- data/lib/parsanol/atoms/infix.rb +167 -0
- data/lib/parsanol/atoms/lookahead.rb +85 -0
- data/lib/parsanol/atoms/named.rb +74 -0
- data/lib/parsanol/atoms/re.rb +83 -0
- data/lib/parsanol/atoms/repetition.rb +277 -0
- data/lib/parsanol/atoms/scope.rb +35 -0
- data/lib/parsanol/atoms/sequence.rb +195 -0
- data/lib/parsanol/atoms/str.rb +109 -0
- data/lib/parsanol/atoms/visitor.rb +91 -0
- data/lib/parsanol/atoms.rb +46 -0
- data/lib/parsanol/buffer.rb +133 -0
- data/lib/parsanol/builder_callbacks.rb +353 -0
- data/lib/parsanol/cause.rb +122 -0
- data/lib/parsanol/context.rb +39 -0
- data/lib/parsanol/convenience.rb +36 -0
- data/lib/parsanol/edit_tracker.rb +111 -0
- data/lib/parsanol/error_reporter/contextual.rb +99 -0
- data/lib/parsanol/error_reporter/deepest.rb +120 -0
- data/lib/parsanol/error_reporter/tree.rb +63 -0
- data/lib/parsanol/error_reporter.rb +100 -0
- data/lib/parsanol/expression/treetop.rb +154 -0
- data/lib/parsanol/expression.rb +106 -0
- data/lib/parsanol/fast_mode.rb +187 -0
- data/lib/parsanol/first_set.rb +79 -0
- data/lib/parsanol/grammar_builder.rb +179 -0
- data/lib/parsanol/incremental_parser.rb +182 -0
- data/lib/parsanol/interval_tree.rb +226 -0
- data/lib/parsanol/lazy_result.rb +179 -0
- data/lib/parsanol/mermaid.rb +142 -0
- data/lib/parsanol/native/batch_decoder.rb +255 -0
- data/lib/parsanol/native/dynamic.rb +238 -0
- data/lib/parsanol/native/parser.rb +102 -0
- data/lib/parsanol/native/serializer.rb +252 -0
- data/lib/parsanol/native/transformer.rb +604 -0
- data/lib/parsanol/native/types.rb +29 -0
- data/lib/parsanol/native.rb +223 -0
- data/lib/parsanol/optimizer.rb +85 -0
- data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
- data/lib/parsanol/optimizers/cut_inserter.rb +182 -0
- data/lib/parsanol/optimizers/lookahead_optimizer.rb +56 -0
- data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
- data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
- data/lib/parsanol/options/zero_copy.rb +127 -0
- data/lib/parsanol/options.rb +21 -0
- data/lib/parsanol/parallel.rb +128 -0
- data/lib/parsanol/parser.rb +242 -0
- data/lib/parsanol/parslet.rb +151 -0
- data/lib/parsanol/pattern/binding.rb +91 -0
- data/lib/parsanol/pattern.rb +162 -0
- data/lib/parsanol/pool.rb +219 -0
- data/lib/parsanol/pools/array_pool.rb +75 -0
- data/lib/parsanol/pools/buffer_pool.rb +182 -0
- data/lib/parsanol/pools/position_pool.rb +92 -0
- data/lib/parsanol/pools/slice_pool.rb +64 -0
- data/lib/parsanol/position.rb +94 -0
- data/lib/parsanol/resettable.rb +29 -0
- data/lib/parsanol/result.rb +46 -0
- data/lib/parsanol/result_builder.rb +208 -0
- data/lib/parsanol/result_stream.rb +266 -0
- data/lib/parsanol/rig/rspec.rb +71 -0
- data/lib/parsanol/rope.rb +81 -0
- data/lib/parsanol/scope.rb +104 -0
- data/lib/parsanol/slice.rb +160 -0
- data/lib/parsanol/source/line_cache.rb +102 -0
- data/lib/parsanol/source.rb +185 -0
- data/lib/parsanol/source_location.rb +167 -0
- data/lib/parsanol/streaming_parser.rb +124 -0
- data/lib/parsanol/string_view.rb +198 -0
- data/lib/parsanol/transform.rb +226 -0
- data/lib/parsanol/version.rb +5 -0
- data/lib/parsanol/wasm/README.md +80 -0
- data/lib/parsanol/wasm/package.json +51 -0
- data/lib/parsanol/wasm/parsanol.js +252 -0
- data/lib/parsanol/wasm/parslet.d.ts +129 -0
- data/lib/parsanol/wasm_parser.rb +240 -0
- data/lib/parsanol.rb +278 -0
- data/parsanol.gemspec +67 -0
- metadata +279 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parsanol/native/transformer"
|
|
4
|
+
|
|
5
|
+
module Parsanol
|
|
6
|
+
module Native
|
|
7
|
+
# Decodes flat u64 arrays from Rust batch parser into Ruby AST
|
|
8
|
+
#
|
|
9
|
+
# The batch format uses tagged u64 values:
|
|
10
|
+
# - 0x00 = nil
|
|
11
|
+
# - 0x01 + value = bool (0 or 1)
|
|
12
|
+
# - 0x02 + value = int
|
|
13
|
+
# - 0x03 + bits = float (IEEE 754 bits)
|
|
14
|
+
# - 0x04 + offset + length = input string reference
|
|
15
|
+
# - 0x05 ... 0x06 = array (start ... end)
|
|
16
|
+
# - 0x07 ... 0x08 = hash (start ... end)
|
|
17
|
+
# - 0x09 + len + data... = hash key
|
|
18
|
+
# - 0x0A + len + data... = inline string
|
|
19
|
+
module BatchDecoder
|
|
20
|
+
TAG_NIL = 0x00
|
|
21
|
+
TAG_BOOL = 0x01
|
|
22
|
+
TAG_INT = 0x02
|
|
23
|
+
TAG_FLOAT = 0x03
|
|
24
|
+
TAG_STRING = 0x04
|
|
25
|
+
TAG_ARRAY_START = 0x05
|
|
26
|
+
TAG_ARRAY_END = 0x06
|
|
27
|
+
TAG_HASH_START = 0x07
|
|
28
|
+
TAG_HASH_END = 0x08
|
|
29
|
+
TAG_HASH_KEY = 0x09
|
|
30
|
+
TAG_INLINE_STRING = 0x0A
|
|
31
|
+
TAG_SYMBOL = 0x0B
|
|
32
|
+
TAG_REPETITION = 0x0C
|
|
33
|
+
TAG_SEQUENCE = 0x0D
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Decode a flat u64 array into Ruby AST with Slice objects
|
|
37
|
+
#
|
|
38
|
+
# @param data [Array<Integer>] Flat u64 array from batch parser
|
|
39
|
+
# @param input [String] Original input string (for Slice references)
|
|
40
|
+
# @param slice_class [Class] The Slice class to use
|
|
41
|
+
# @return [Object] Ruby AST (Hash, Array, Slice, etc.)
|
|
42
|
+
def decode(data, input, slice_class)
|
|
43
|
+
@input = input
|
|
44
|
+
@input_bytes = input.b
|
|
45
|
+
@slice_class = slice_class
|
|
46
|
+
@pos = 0
|
|
47
|
+
@data = data
|
|
48
|
+
decode_value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Decode batch format to Ruby AST and apply transformation.
|
|
52
|
+
#
|
|
53
|
+
# The Rust parser produces raw AST that needs transformation to match
|
|
54
|
+
# Ruby parser behavior (merging duplicate keys, etc.)
|
|
55
|
+
#
|
|
56
|
+
# @param data [Array<Integer>|Object] Either flat u64 array from batch parser OR
|
|
57
|
+
# pre-decoded Ruby value from _parse_raw
|
|
58
|
+
# @param input [String] Original input string (for Slice references)
|
|
59
|
+
# @param slice_class [Class] The Slice class to use
|
|
60
|
+
# @return [Object] Transformed Ruby AST
|
|
61
|
+
def decode_and_flatten(data, input, slice_class)
|
|
62
|
+
# Check if data is batch data (flat u64 array) or already a Ruby value
|
|
63
|
+
if data.is_a?(Integer) || (data.is_a?(Array) && data.first.is_a?(Integer))
|
|
64
|
+
# Batch data (flat u64 array) - decode first, then transform
|
|
65
|
+
raw_ast = decode(data, input, slice_class)
|
|
66
|
+
AstTransformer.transform(raw_ast)
|
|
67
|
+
else
|
|
68
|
+
# Already decoded Ruby value from _parse_raw - apply transformer directly
|
|
69
|
+
AstTransformer.transform(data)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Join consecutive Slice objects in arrays into single Slices
|
|
74
|
+
# This matches what transform_ast does in Rust (join_slices_from_array)
|
|
75
|
+
#
|
|
76
|
+
# @param value [Object] AST value
|
|
77
|
+
# @param slice_class [Class] The Slice class to check for
|
|
78
|
+
# @param input [String] Original input string
|
|
79
|
+
# @return [Object] AST with joined slices
|
|
80
|
+
def join_consecutive_slices(value, slice_class, input)
|
|
81
|
+
input_bytes = input.b
|
|
82
|
+
|
|
83
|
+
case value
|
|
84
|
+
when Array
|
|
85
|
+
# Recursively process array elements
|
|
86
|
+
processed = value.map do |v|
|
|
87
|
+
join_consecutive_slices(v, slice_class, input)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if all non-nil elements are Slices
|
|
91
|
+
non_nil = processed.compact
|
|
92
|
+
if non_nil.all?(slice_class)
|
|
93
|
+
# Check if slices are consecutive
|
|
94
|
+
if slices_consecutive?(non_nil)
|
|
95
|
+
# Join into single slice
|
|
96
|
+
join_slices(non_nil, slice_class, input_bytes, input)
|
|
97
|
+
else
|
|
98
|
+
processed
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
processed
|
|
102
|
+
end
|
|
103
|
+
when Hash
|
|
104
|
+
# Process hash values recursively
|
|
105
|
+
result = {}
|
|
106
|
+
value.each do |k, v|
|
|
107
|
+
result[k] = join_consecutive_slices(v, slice_class, input)
|
|
108
|
+
end
|
|
109
|
+
result
|
|
110
|
+
else
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def slices_consecutive?(slices)
|
|
118
|
+
return true if slices.empty?
|
|
119
|
+
|
|
120
|
+
slices.each_cons(2).all? do |a, b|
|
|
121
|
+
a.offset + a.content.bytesize == b.offset
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def join_slices(slices, slice_class, input_bytes, input)
|
|
126
|
+
return nil if slices.empty?
|
|
127
|
+
return slices.first if slices.length == 1
|
|
128
|
+
|
|
129
|
+
first = slices.first
|
|
130
|
+
last = slices.last
|
|
131
|
+
total_length = last.offset + last.content.bytesize - first.offset
|
|
132
|
+
content = input_bytes[first.offset, total_length]
|
|
133
|
+
content = content.force_encoding("UTF-8") if content
|
|
134
|
+
slice_class.new(first.offset, content, input)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def decode_value
|
|
138
|
+
tag = @data[@pos]
|
|
139
|
+
@pos += 1
|
|
140
|
+
|
|
141
|
+
case tag
|
|
142
|
+
when TAG_NIL
|
|
143
|
+
nil
|
|
144
|
+
when TAG_BOOL
|
|
145
|
+
val = @data[@pos]
|
|
146
|
+
@pos += 1
|
|
147
|
+
val != 0
|
|
148
|
+
when TAG_INT
|
|
149
|
+
val = @data[@pos]
|
|
150
|
+
@pos += 1
|
|
151
|
+
# Handle negative numbers (signed i64 stored as u64)
|
|
152
|
+
if val >= 0x8000_0000_0000_0000
|
|
153
|
+
val = val - 0x1_0000_0000_0000_0000
|
|
154
|
+
end
|
|
155
|
+
val
|
|
156
|
+
when TAG_FLOAT
|
|
157
|
+
bits = @data[@pos]
|
|
158
|
+
@pos += 1
|
|
159
|
+
# Convert IEEE 754 bits to float
|
|
160
|
+
[bits].pack("Q").unpack1("D")
|
|
161
|
+
when TAG_STRING
|
|
162
|
+
offset = @data[@pos]
|
|
163
|
+
length = @data[@pos + 1]
|
|
164
|
+
@pos += 2
|
|
165
|
+
create_slice(offset, length)
|
|
166
|
+
when TAG_SYMBOL
|
|
167
|
+
# Symbol is encoded like inline string: len, then u64 chunks
|
|
168
|
+
len = @data[@pos]
|
|
169
|
+
@pos += 1
|
|
170
|
+
str = decode_inline_string_bytes(len)
|
|
171
|
+
str.to_sym
|
|
172
|
+
when TAG_REPETITION
|
|
173
|
+
inner = decode_value
|
|
174
|
+
[:repetition, inner].compact
|
|
175
|
+
when TAG_SEQUENCE
|
|
176
|
+
inner = decode_value
|
|
177
|
+
[:sequence, inner].compact
|
|
178
|
+
when TAG_ARRAY_START
|
|
179
|
+
decode_array
|
|
180
|
+
when TAG_HASH_START
|
|
181
|
+
decode_hash
|
|
182
|
+
else
|
|
183
|
+
raise "Unknown tag: #{tag} at position #{@pos - 1}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def decode_array
|
|
188
|
+
result = []
|
|
189
|
+
loop do
|
|
190
|
+
tag = @data[@pos]
|
|
191
|
+
break if tag == TAG_ARRAY_END
|
|
192
|
+
|
|
193
|
+
result << decode_value
|
|
194
|
+
end
|
|
195
|
+
@pos += 1 # consume TAG_ARRAY_END
|
|
196
|
+
result
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def decode_hash
|
|
200
|
+
result = {}
|
|
201
|
+
loop do
|
|
202
|
+
tag = @data[@pos]
|
|
203
|
+
break if tag == TAG_HASH_END
|
|
204
|
+
|
|
205
|
+
# Read key
|
|
206
|
+
raise "Expected TAG_HASH_KEY, got #{tag}" unless tag == TAG_HASH_KEY
|
|
207
|
+
|
|
208
|
+
@pos += 1
|
|
209
|
+
key = decode_inline_string
|
|
210
|
+
|
|
211
|
+
# Read value
|
|
212
|
+
value = decode_value
|
|
213
|
+
|
|
214
|
+
# Keep original key format (camelCase) for Ruby parser compatibility
|
|
215
|
+
result[key.to_sym] = value
|
|
216
|
+
end
|
|
217
|
+
@pos += 1 # consume TAG_HASH_END
|
|
218
|
+
result
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def decode_inline_string
|
|
222
|
+
len = @data[@pos]
|
|
223
|
+
@pos += 1
|
|
224
|
+
decode_inline_string_bytes(len)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Decode inline string bytes given the length
|
|
228
|
+
# @param len [Integer] Length of the string in bytes
|
|
229
|
+
# @return [String] Decoded string
|
|
230
|
+
def decode_inline_string_bytes(len)
|
|
231
|
+
# Read u64 chunks
|
|
232
|
+
chunks = (len + 7) / 8
|
|
233
|
+
bytes = String.new(encoding: "ASCII-8BIT", capacity: len)
|
|
234
|
+
chunks.times do
|
|
235
|
+
chunk = @data[@pos]
|
|
236
|
+
@pos += 1
|
|
237
|
+
8.times do |byte_idx|
|
|
238
|
+
break if bytes.bytesize >= len
|
|
239
|
+
|
|
240
|
+
bytes << ((chunk >> (byte_idx * 8)) & 0xFF)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
bytes.force_encoding("UTF-8")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def create_slice(offset, length)
|
|
248
|
+
content = @input_bytes[offset, length]
|
|
249
|
+
content = content.force_encoding("UTF-8") if content
|
|
250
|
+
@slice_class.new(offset, content, @input)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Parsanol
|
|
6
|
+
module Native
|
|
7
|
+
# Manages Ruby callbacks for dynamic atoms
|
|
8
|
+
#
|
|
9
|
+
# Dynamic atoms allow runtime-determined parsing by invoking Ruby code
|
|
10
|
+
# during parsing. This module provides:
|
|
11
|
+
# - Registration of Ruby procs as callbacks
|
|
12
|
+
# - Thread-safe callback storage
|
|
13
|
+
# - GC-safe references (callbacks are kept alive while registered)
|
|
14
|
+
#
|
|
15
|
+
# @example Basic usage
|
|
16
|
+
# # Register a callback
|
|
17
|
+
# callback_id = Parsanol::Native::Dynamic.register(->(ctx) {
|
|
18
|
+
# ctx[:mode] == 'A' ? str('alpha') : str('beta')
|
|
19
|
+
# })
|
|
20
|
+
#
|
|
21
|
+
# # Use in grammar
|
|
22
|
+
# grammar = str('MODE:').capture(:mode) >> dynamic(callback_id)
|
|
23
|
+
#
|
|
24
|
+
# # Unregister when done
|
|
25
|
+
# Parsanol::Native::Dynamic.unregister(callback_id)
|
|
26
|
+
#
|
|
27
|
+
module Dynamic
|
|
28
|
+
# Callback storage (callback_id => block)
|
|
29
|
+
# This keeps strong references to prevent GC
|
|
30
|
+
@callbacks = {}
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
@next_id = 1_000_000 # Start high to avoid conflicts with Rust-side IDs
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Register a Ruby block as a dynamic callback
|
|
36
|
+
#
|
|
37
|
+
# @param block [Proc] The block to register (must accept a context hash)
|
|
38
|
+
# @param description [String, nil] Optional description for debugging
|
|
39
|
+
# @return [Integer] Unique callback ID for use in grammar
|
|
40
|
+
#
|
|
41
|
+
# @example
|
|
42
|
+
# id = Parsanol::Native::Dynamic.register(->(ctx) {
|
|
43
|
+
# case ctx[:type]
|
|
44
|
+
# when 'int' then str('integer')
|
|
45
|
+
# when 'str' then str('string')
|
|
46
|
+
# else nil
|
|
47
|
+
# end
|
|
48
|
+
# })
|
|
49
|
+
#
|
|
50
|
+
def register(block, description: nil)
|
|
51
|
+
# Register with Rust FFI
|
|
52
|
+
ffi_id = Native.register_callback(@next_id,
|
|
53
|
+
description || "Ruby callback ##{@next_id}")
|
|
54
|
+
|
|
55
|
+
# Also keep a Ruby-side reference for GC safety
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
@callbacks[ffi_id] = {
|
|
58
|
+
block: block,
|
|
59
|
+
description: description || "Ruby callback ##{ffi_id}",
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
ffi_id
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Unregister a callback (free memory)
|
|
67
|
+
#
|
|
68
|
+
# @param callback_id [Integer] The callback ID to remove
|
|
69
|
+
# @return [Boolean] True if the callback was found and removed
|
|
70
|
+
#
|
|
71
|
+
def unregister(callback_id)
|
|
72
|
+
# Remove from Rust FFI
|
|
73
|
+
Native.unregister_callback(callback_id)
|
|
74
|
+
|
|
75
|
+
# Remove from Ruby storage
|
|
76
|
+
@mutex.synchronize do
|
|
77
|
+
@callbacks.delete(callback_id)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get the description of a registered callback
|
|
82
|
+
#
|
|
83
|
+
# @param callback_id [Integer] The callback ID
|
|
84
|
+
# @return [String, nil] The description or nil if not found
|
|
85
|
+
#
|
|
86
|
+
def description(callback_id)
|
|
87
|
+
# Try Ruby-side first
|
|
88
|
+
ruby_desc = @mutex.synchronize do
|
|
89
|
+
@callbacks[callback_id]&.dig(:description)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return ruby_desc if ruby_desc
|
|
93
|
+
|
|
94
|
+
# Fall back to FFI
|
|
95
|
+
Native.get_callback_description(callback_id)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get the number of registered callbacks
|
|
99
|
+
#
|
|
100
|
+
# @return [Integer] Number of registered callbacks
|
|
101
|
+
#
|
|
102
|
+
def count
|
|
103
|
+
Native.callback_count
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Clear all callbacks (for testing)
|
|
107
|
+
#
|
|
108
|
+
# WARNING: This clears all callbacks globally, including those
|
|
109
|
+
# registered by other code. Use with caution.
|
|
110
|
+
#
|
|
111
|
+
def clear
|
|
112
|
+
Native.clear_callbacks
|
|
113
|
+
@mutex.synchronize { @callbacks.clear }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check if a callback is registered
|
|
117
|
+
#
|
|
118
|
+
# @param callback_id [Integer] The callback ID
|
|
119
|
+
# @return [Boolean] True if registered
|
|
120
|
+
#
|
|
121
|
+
def registered?(callback_id)
|
|
122
|
+
@mutex.synchronize { @callbacks.key?(callback_id) } ||
|
|
123
|
+
Native.has_callback(callback_id)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Invoke a callback from Rust (called via FFI)
|
|
127
|
+
#
|
|
128
|
+
# @param callback_id [Integer] The callback ID
|
|
129
|
+
# @param context [Hash] The context hash from Rust
|
|
130
|
+
# @return [Object, nil] The returned atom (parslet) or nil
|
|
131
|
+
#
|
|
132
|
+
def invoke_from_rust(callback_id, context)
|
|
133
|
+
block = @mutex.synchronize { @callbacks[callback_id]&.dig(:block) }
|
|
134
|
+
return nil unless block
|
|
135
|
+
|
|
136
|
+
# Build DynamicContext from hash
|
|
137
|
+
ctx = DynamicContext.new(
|
|
138
|
+
context[:input],
|
|
139
|
+
context[:pos],
|
|
140
|
+
context[:captures].transform_keys(&:to_sym),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Call the block
|
|
144
|
+
result = block.call(ctx)
|
|
145
|
+
|
|
146
|
+
return nil unless result
|
|
147
|
+
|
|
148
|
+
# Return the result (should be a parslet/atom)
|
|
149
|
+
result
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
warn "[Parsanol::Native::Dynamic] Invoke error: #{e.message}"
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Context object passed to dynamic callbacks
|
|
158
|
+
#
|
|
159
|
+
# Provides read-only access to the parsing context including
|
|
160
|
+
# input string, current position, and captured values.
|
|
161
|
+
#
|
|
162
|
+
# @example
|
|
163
|
+
# dynamic { |ctx|
|
|
164
|
+
# if ctx[:mode] == 'strict'
|
|
165
|
+
# str('strict_value')
|
|
166
|
+
# else
|
|
167
|
+
# str('relaxed_value')
|
|
168
|
+
# end
|
|
169
|
+
# }
|
|
170
|
+
#
|
|
171
|
+
class DynamicContext
|
|
172
|
+
# @return [String] The full input string being parsed
|
|
173
|
+
attr_reader :input
|
|
174
|
+
|
|
175
|
+
# @return [Integer] Current byte position in the input
|
|
176
|
+
attr_reader :pos
|
|
177
|
+
|
|
178
|
+
# @return [Hash<Symbol, String>] Captured values
|
|
179
|
+
attr_reader :captures
|
|
180
|
+
|
|
181
|
+
def initialize(input, pos, captures)
|
|
182
|
+
@input = input
|
|
183
|
+
@pos = pos
|
|
184
|
+
@captures = captures.transform_keys(&:to_sym)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Get a captured value by name
|
|
188
|
+
#
|
|
189
|
+
# @param name [Symbol, String] The capture name
|
|
190
|
+
# @return [String, nil] The captured value or nil
|
|
191
|
+
#
|
|
192
|
+
def [](name)
|
|
193
|
+
@captures[name.to_sym]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check if a capture exists
|
|
197
|
+
#
|
|
198
|
+
# @param name [Symbol, String] The capture name
|
|
199
|
+
# @return [Boolean] True if the capture exists
|
|
200
|
+
#
|
|
201
|
+
def key?(name)
|
|
202
|
+
@captures.key?(name.to_sym)
|
|
203
|
+
end
|
|
204
|
+
alias has_key? key?
|
|
205
|
+
|
|
206
|
+
# Get the remaining input from the current position
|
|
207
|
+
#
|
|
208
|
+
# @return [String] The remaining input
|
|
209
|
+
#
|
|
210
|
+
def remaining
|
|
211
|
+
@input[@pos..] || ""
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Check if at end of input
|
|
215
|
+
#
|
|
216
|
+
# @return [Boolean] True if at end
|
|
217
|
+
#
|
|
218
|
+
def eos?
|
|
219
|
+
@pos >= @input.length
|
|
220
|
+
end
|
|
221
|
+
alias at_end? eos?
|
|
222
|
+
|
|
223
|
+
# Get a slice of the input
|
|
224
|
+
#
|
|
225
|
+
# @param start [Integer] Start position (relative to current pos if negative)
|
|
226
|
+
# @param length [Integer, nil] Length of slice (nil = to end)
|
|
227
|
+
# @return [String] The sliced input
|
|
228
|
+
#
|
|
229
|
+
def slice(start, length = nil)
|
|
230
|
+
if length
|
|
231
|
+
@input[@pos + start, length]
|
|
232
|
+
else
|
|
233
|
+
@input[(@pos + start)..]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Parsanol
|
|
6
|
+
module Native
|
|
7
|
+
# Core parsing functionality using Rust native extension
|
|
8
|
+
module Parser
|
|
9
|
+
GRAMMAR_HASH_CACHE = Hash.new
|
|
10
|
+
GRAMMAR_CACHE = Hash.new
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
@cached_available = nil
|
|
14
|
+
|
|
15
|
+
def available?
|
|
16
|
+
return @cached_available unless @cached_available.nil?
|
|
17
|
+
|
|
18
|
+
@cached_available = begin
|
|
19
|
+
# Try versioned path first (released gem), then non-versioned (local dev)
|
|
20
|
+
ruby_version = RUBY_VERSION.split(".").take(2).join(".")
|
|
21
|
+
begin
|
|
22
|
+
require "parsanol/#{ruby_version}/parsanol_native"
|
|
23
|
+
rescue LoadError
|
|
24
|
+
require "parsanol/parsanol_native"
|
|
25
|
+
end
|
|
26
|
+
Parsanol::Native.is_available
|
|
27
|
+
rescue LoadError
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Parse input with a Ruby grammar, returning clean AST.
|
|
33
|
+
#
|
|
34
|
+
# @param grammar [Parsanol::Atoms::Base] Ruby grammar or JSON string
|
|
35
|
+
# @param input [String] Input string to parse
|
|
36
|
+
def parse(grammar, input)
|
|
37
|
+
# Delegate to Parsanol::Native.parse for consistency
|
|
38
|
+
Parsanol::Native.parse(grammar, input)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Serialize a Ruby grammar to JSON (cached).
|
|
42
|
+
def serialize_grammar(root_atom)
|
|
43
|
+
root_atom = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
|
|
44
|
+
obj_id = root_atom.object_id
|
|
45
|
+
cache_key = GRAMMAR_HASH_CACHE[obj_id] ||= grammar_structure_hash(root_atom)
|
|
46
|
+
GRAMMAR_CACHE[cache_key] ||= GrammarSerializer.serialize(root_atom)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def clear_cache
|
|
50
|
+
GRAMMAR_HASH_CACHE.clear
|
|
51
|
+
GRAMMAR_CACHE.clear
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cache_stats
|
|
55
|
+
{
|
|
56
|
+
hash_cache_size: GRAMMAR_HASH_CACHE.size,
|
|
57
|
+
grammar_cache_size: GRAMMAR_CACHE.size,
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def grammar_structure_hash(atom)
|
|
64
|
+
Digest::MD5.hexdigest(atom_structure(atom).to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def atom_structure(atom, visited = {})
|
|
68
|
+
# Cycle detection - return a placeholder if we've seen this atom before
|
|
69
|
+
obj_id = atom.object_id
|
|
70
|
+
if visited[obj_id]
|
|
71
|
+
return [:cycle, atom.class.name]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
visited[obj_id] = true
|
|
75
|
+
|
|
76
|
+
case atom
|
|
77
|
+
when ::Parsanol::Atoms::Entity
|
|
78
|
+
# Recursively resolve entity to get actual structure for hash
|
|
79
|
+
atom_structure(atom.parslet, visited)
|
|
80
|
+
when ::Parsanol::Atoms::Str
|
|
81
|
+
[:str, atom.str]
|
|
82
|
+
when ::Parsanol::Atoms::Re
|
|
83
|
+
[:re, atom.match]
|
|
84
|
+
when ::Parsanol::Atoms::Sequence
|
|
85
|
+
[:seq, atom.parslets.map { |p| atom_structure(p, visited) }]
|
|
86
|
+
when ::Parsanol::Atoms::Alternative
|
|
87
|
+
[:alt, atom.alternatives.map { |p| atom_structure(p, visited) }]
|
|
88
|
+
when ::Parsanol::Atoms::Repetition
|
|
89
|
+
[:rep, atom.min, atom.max, atom_structure(atom.parslet, visited)]
|
|
90
|
+
when ::Parsanol::Atoms::Named
|
|
91
|
+
[:named, atom.name.to_s, atom_structure(atom.parslet, visited)]
|
|
92
|
+
when ::Parsanol::Atoms::Lookahead
|
|
93
|
+
[:lookahead, atom.positive,
|
|
94
|
+
atom_structure(atom.bound_parslet, visited)]
|
|
95
|
+
else
|
|
96
|
+
[:unknown, atom.class.name]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|