parsanol 1.3.36 → 1.3.37
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 +4 -4
- data/Cargo.lock +6 -4
- data/ext/parsanol_native/Cargo.toml +1 -1
- data/lib/parsanol/atoms/base.rb +9 -1
- data/lib/parsanol/atoms/can_flatten.rb +4 -1
- data/lib/parsanol/atoms/re.rb +27 -5
- data/lib/parsanol/native/event_player.rb +93 -0
- data/lib/parsanol/native/parser.rb +14 -0
- data/lib/parsanol/native.rb +1 -0
- data/lib/parsanol/parser.rb +11 -8
- data/lib/parsanol/scope.rb +14 -0
- data/lib/parsanol/source.rb +53 -0
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol.rb +0 -21
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3ee6550442261f9f42d66c0b51e9b5fe5f186c54c9b36a2932abc0410608a42
|
|
4
|
+
data.tar.gz: 9f0476cfd0d65c1642c81bf86bd36a2a0a6627d1c2978ba65f00989e84e6bd44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1dbb402205ebfb234148f7f87b5d9bcf6cd201b7f72f3c73bd096c7a0c3ba092582ad1a37a543f1665fd2542d8b2c6841b5ca9e4f21525a2ecfac68e343bad7
|
|
7
|
+
data.tar.gz: dc490cc9e39a4feb101139c1fd9a2d47e5509d495cb99920f6856cf65eac3303147e8cb95c13b25f8086d2fc31cb77be5193438527e28d4d261f08f28a7842cb
|
data/Cargo.lock
CHANGED
|
@@ -252,8 +252,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
252
252
|
|
|
253
253
|
[[package]]
|
|
254
254
|
name = "parsanol"
|
|
255
|
-
version = "0.7.
|
|
256
|
-
source = "
|
|
255
|
+
version = "0.7.3"
|
|
256
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
257
|
+
checksum = "1f87f23ea48eb961366d013b90eac82e8f1d6e129d102ab33a7d56f5ec26a883"
|
|
257
258
|
dependencies = [
|
|
258
259
|
"ahash",
|
|
259
260
|
"getrandom 0.3.4",
|
|
@@ -269,8 +270,9 @@ dependencies = [
|
|
|
269
270
|
|
|
270
271
|
[[package]]
|
|
271
272
|
name = "parsanol-derive"
|
|
272
|
-
version = "0.7.
|
|
273
|
-
source = "
|
|
273
|
+
version = "0.7.3"
|
|
274
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
275
|
+
checksum = "a4043d26b37aa72b4a8efc02e6dc0a7225cb28e484b07bd290ede35c8a06a566"
|
|
274
276
|
dependencies = [
|
|
275
277
|
"proc-macro2",
|
|
276
278
|
"quote",
|
|
@@ -28,7 +28,7 @@ rb-sys = { version = "0.9.124", features = ["global-allocator"] }
|
|
|
28
28
|
magnus = { version = "0.9" }
|
|
29
29
|
|
|
30
30
|
# parsanol parser library (from git for latest features)
|
|
31
|
-
parsanol = {
|
|
31
|
+
parsanol = { version = "0.7.3", features = ["ruby"] }
|
|
32
32
|
|
|
33
33
|
# Logging
|
|
34
34
|
log = "0.4"
|
data/lib/parsanol/atoms/base.rb
CHANGED
|
@@ -39,7 +39,15 @@ module Parsanol
|
|
|
39
39
|
# grammars transparently.
|
|
40
40
|
if must_consume_all && source.is_a?(String) &&
|
|
41
41
|
(program = VM.program_for(self))
|
|
42
|
-
|
|
42
|
+
begin
|
|
43
|
+
result = VM.run_for(self, program, source, true)
|
|
44
|
+
rescue ArgumentError, TypeError
|
|
45
|
+
# Executor crash on a malformed program (GH-68): the
|
|
46
|
+
# interpreter is the source of truth — fall through and
|
|
47
|
+
# stop using the VM for this grammar.
|
|
48
|
+
VM.disable_for!(self)
|
|
49
|
+
result = VM::BAIL
|
|
50
|
+
end
|
|
43
51
|
if result == VM::BAIL
|
|
44
52
|
# Internal bail: fall back to the interpreter and skip the VM
|
|
45
53
|
# for this grammar from now on.
|
|
@@ -216,7 +216,10 @@ module Parsanol
|
|
|
216
216
|
end
|
|
217
217
|
piece = e.instance_of?(Parsanol::Slice) ? e.content : e.to_s
|
|
218
218
|
if content.nil?
|
|
219
|
-
|
|
219
|
+
# Copy: unary + returns self for unfrozen strings, and
|
|
220
|
+
# appending into an input slice's buffer corrupts it when
|
|
221
|
+
# the same cached subtree is flattened again (GH-67).
|
|
222
|
+
content = String.new(piece)
|
|
220
223
|
else
|
|
221
224
|
content << piece
|
|
222
225
|
end
|
data/lib/parsanol/atoms/re.rb
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Regular expression matcher
|
|
4
|
-
#
|
|
3
|
+
# Regular expression matcher.
|
|
4
|
+
#
|
|
5
|
+
# A plain character class matches one character. Like Parslet, a
|
|
6
|
+
# pattern with an embedded quantifier (match('a+') etc.) is a regex
|
|
7
|
+
# matched greedily at the current position: the whole regex match is
|
|
8
|
+
# consumed in one step.
|
|
5
9
|
#
|
|
6
10
|
# @example Character classes
|
|
7
|
-
# match('[a-z]') # matches a-z
|
|
8
|
-
# match('
|
|
11
|
+
# match('[a-z]') # matches one a-z character
|
|
12
|
+
# match('a+') # matches a maximal run of a (Parslet parity)
|
|
13
|
+
# match('\d') # matches one digit
|
|
9
14
|
# any # matches any character
|
|
10
15
|
#
|
|
11
16
|
module Parsanol
|
|
@@ -20,10 +25,15 @@ module Parsanol
|
|
|
20
25
|
# Creates a new regex matcher.
|
|
21
26
|
#
|
|
22
27
|
# @param pattern [String, Object] regex character class
|
|
28
|
+
QUANTIFIER_RE = /[+*?]|\{\d+(?:,\s*\d*)?\}/
|
|
29
|
+
|
|
23
30
|
def initialize(pattern)
|
|
24
31
|
super()
|
|
25
32
|
@match = pattern.to_s
|
|
26
33
|
@re = Regexp.new(@match, Regexp::MULTILINE)
|
|
34
|
+
# Embedded quantifier: one greedy regex match instead of a
|
|
35
|
+
# single character (Parslet parity, GH-69).
|
|
36
|
+
@quantified = QUANTIFIER_RE.match?(@match)
|
|
27
37
|
|
|
28
38
|
# Extract pattern for display (strip delimiters)
|
|
29
39
|
@display = @match.inspect[1..-2] || @match
|
|
@@ -40,7 +50,19 @@ module Parsanol
|
|
|
40
50
|
# @param _consume_all [Boolean] ignored
|
|
41
51
|
# @return [Array(Boolean, Object)] result
|
|
42
52
|
def try(source, context, _consume_all)
|
|
43
|
-
|
|
53
|
+
if @quantified
|
|
54
|
+
# One greedy regex match (Parslet parity): consume the whole
|
|
55
|
+
# match. A zero-length match is treated as no match so
|
|
56
|
+
# repetitions always advance.
|
|
57
|
+
matched = source.match_bytes(@re)
|
|
58
|
+
return ok(source.consume_bytes(matched)) if matched&.positive?
|
|
59
|
+
|
|
60
|
+
return context.err(self, source, @eof_error) if source.chars_left < 1
|
|
61
|
+
|
|
62
|
+
return context.err(self, source, @no_match_error)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Fast path: single-character class
|
|
44
66
|
return ok(source.consume(1)) if source.matches?(@re)
|
|
45
67
|
|
|
46
68
|
# No input left
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module Native
|
|
5
|
+
# Reference consumer for the flat event stream returned by
|
|
6
|
+
# {Parsanol::Native::Parser.parse_events}: replays the opcode
|
|
7
|
+
# stream into the same tree Parsanol::Native.parse produces.
|
|
8
|
+
#
|
|
9
|
+
# Domain builders (e.g. an EXPRESS model builder) should not
|
|
10
|
+
# rebuild this tree — they consume the events directly, which is
|
|
11
|
+
# the point of the format. This player exists to document the
|
|
12
|
+
# protocol and to conformance-test the stream against the shaped
|
|
13
|
+
# tree.
|
|
14
|
+
#
|
|
15
|
+
# Opcodes (see parsanol-rs portable::events):
|
|
16
|
+
# 0 NIL 4 BEGIN_ARR 6 BEGIN_HASH
|
|
17
|
+
# 1 STR <pool> 5 END_ARR 7 END_HASH
|
|
18
|
+
# 2 SLICE <offset> <len> 3 KEY <pool>
|
|
19
|
+
class EventPlayer
|
|
20
|
+
OP_NIL = 0
|
|
21
|
+
OP_STR = 1
|
|
22
|
+
OP_SLICE = 2
|
|
23
|
+
OP_KEY = 3
|
|
24
|
+
OP_BEGIN_ARR = 4
|
|
25
|
+
OP_END_ARR = 5
|
|
26
|
+
OP_BEGIN_HASH = 6
|
|
27
|
+
OP_END_HASH = 7
|
|
28
|
+
|
|
29
|
+
# One-shot replay: returns the tree for the stream.
|
|
30
|
+
def self.play(events, strings, input)
|
|
31
|
+
player = new(events, strings, input)
|
|
32
|
+
tree = player.read_value
|
|
33
|
+
unless player.pos == events.length
|
|
34
|
+
raise ArgumentError, "event stream not fully consumed"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
tree
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(events, strings, input)
|
|
41
|
+
@events = events
|
|
42
|
+
@strings = strings
|
|
43
|
+
@input = input
|
|
44
|
+
@pos = 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :pos
|
|
48
|
+
|
|
49
|
+
def read_value
|
|
50
|
+
op = @events[@pos]
|
|
51
|
+
@pos += 1
|
|
52
|
+
case op
|
|
53
|
+
when OP_NIL then nil
|
|
54
|
+
when OP_STR
|
|
55
|
+
value = @strings[@events[@pos]]
|
|
56
|
+
@pos += 1
|
|
57
|
+
value
|
|
58
|
+
when OP_SLICE
|
|
59
|
+
offset = @events[@pos]
|
|
60
|
+
length = @events[@pos + 1]
|
|
61
|
+
@pos += 2
|
|
62
|
+
::Parsanol::Slice.new(offset, @input.byteslice(offset, length), @input)
|
|
63
|
+
when OP_BEGIN_HASH
|
|
64
|
+
hash = {}
|
|
65
|
+
until @events[@pos] == OP_END_HASH
|
|
66
|
+
@pos += 1 # KEY opcode
|
|
67
|
+
key = symbol(@strings[@events[@pos]])
|
|
68
|
+
@pos += 1
|
|
69
|
+
hash[key] = read_value
|
|
70
|
+
end
|
|
71
|
+
@pos += 1
|
|
72
|
+
hash
|
|
73
|
+
when OP_BEGIN_ARR
|
|
74
|
+
array = []
|
|
75
|
+
until @events[@pos] == OP_END_ARR
|
|
76
|
+
array << read_value
|
|
77
|
+
end
|
|
78
|
+
@pos += 1
|
|
79
|
+
array
|
|
80
|
+
else
|
|
81
|
+
raise ArgumentError, "unknown event opcode #{op} at #{@pos - 1}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def symbol(key)
|
|
88
|
+
@@symbol_cache ||= {}
|
|
89
|
+
@@symbol_cache[key] ||= key.to_sym
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -48,6 +48,20 @@ module Parsanol
|
|
|
48
48
|
Parsanol::Native.parse(grammar, input)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Parse input with a Ruby grammar, returning the parslet-shaped
|
|
52
|
+
# AST as a flat event stream [[events], [strings]] — one FFI
|
|
53
|
+
# return, no per-node Ruby objects. Replay with EventPlayer or
|
|
54
|
+
# consume the opcodes directly.
|
|
55
|
+
#
|
|
56
|
+
# @param grammar [Parsanol::Atoms::Base] Ruby grammar definition
|
|
57
|
+
# @param input [String] Input string to parse
|
|
58
|
+
# @return [Array<Array<Integer>, Array<String>>]
|
|
59
|
+
def parse_events(grammar, input)
|
|
60
|
+
handle = grammar_handle(grammar)
|
|
61
|
+
blob, strings = Native._parse_handle_events(handle, input)
|
|
62
|
+
[blob.unpack("q*"), strings]
|
|
63
|
+
end
|
|
64
|
+
|
|
51
65
|
# Serialize a Ruby grammar to JSON (cached).
|
|
52
66
|
def serialize_grammar(root_atom)
|
|
53
67
|
grammar_json(root_atom)
|
data/lib/parsanol/native.rb
CHANGED
data/lib/parsanol/parser.rb
CHANGED
|
@@ -230,22 +230,25 @@ module Parsanol
|
|
|
230
230
|
raise
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
233
|
+
# Public expressibility check (GH-71): true when the Rust native
|
|
234
|
+
# backend can run this grammar, false when it must run on the Ruby
|
|
235
|
+
# engine (Dynamic/custom atoms). Use it to pick an engine
|
|
236
|
+
# explicitly via parse(input, mode: :native | :ruby).
|
|
237
|
+
#
|
|
238
|
+
# The degradation warning prints once per reason per process — not
|
|
239
|
+
# per parser instance and not per parse.
|
|
238
240
|
def native_expressible?
|
|
239
241
|
Parsanol::Native::Parser.grammar_handle(root)
|
|
240
242
|
true
|
|
241
243
|
rescue Parsanol::Native::UnsupportedGrammar => e
|
|
242
|
-
warned = (@@unsupported_warned ||= {}
|
|
243
|
-
unless warned.key?(
|
|
244
|
-
warned[
|
|
244
|
+
warned = (@@unsupported_warned ||= {})
|
|
245
|
+
unless warned.key?(e.message)
|
|
246
|
+
warned[e.message] = true
|
|
245
247
|
warn "parsanol: parsing with the Ruby engine (#{e.message})"
|
|
246
248
|
end
|
|
247
249
|
false
|
|
248
250
|
end
|
|
251
|
+
public :native_expressible?
|
|
249
252
|
|
|
250
253
|
# Feed the native deepest-failure diagnostics to a user-supplied
|
|
251
254
|
# reporter: one err_at event carrying the cause the native engine
|
data/lib/parsanol/scope.rb
CHANGED
|
@@ -52,6 +52,11 @@ module Parsanol
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Chain-aware membership test without raising.
|
|
56
|
+
def key?(key)
|
|
57
|
+
@bindings.key?(key) || (@parent_frame&.key?(key) || false)
|
|
58
|
+
end
|
|
59
|
+
|
|
55
60
|
# Stores a value in the current frame.
|
|
56
61
|
#
|
|
57
62
|
# @param key [Symbol] the variable name
|
|
@@ -79,6 +84,15 @@ module Parsanol
|
|
|
79
84
|
@active_frame.fetch(key)
|
|
80
85
|
end
|
|
81
86
|
|
|
87
|
+
# Chain-aware membership test, mirroring Parslet's captures.key?.
|
|
88
|
+
# Checks the current frame and all parents without raising.
|
|
89
|
+
#
|
|
90
|
+
# @param key [Symbol] the variable name to look up
|
|
91
|
+
# @return [Boolean] whether any frame binds the key
|
|
92
|
+
def key?(key)
|
|
93
|
+
@active_frame.key?(key)
|
|
94
|
+
end
|
|
95
|
+
|
|
82
96
|
# Stores a value in the current frame.
|
|
83
97
|
#
|
|
84
98
|
# @param key [Symbol] the variable name
|
data/lib/parsanol/source.rb
CHANGED
|
@@ -69,6 +69,59 @@ module Parsanol
|
|
|
69
69
|
end
|
|
70
70
|
alias match matches?
|
|
71
71
|
|
|
72
|
+
# Byte length of the pattern's match at the current position
|
|
73
|
+
# (StringScanner#match? returns it), or nil when there is no match.
|
|
74
|
+
#
|
|
75
|
+
# @param pattern [Regexp] pattern to probe
|
|
76
|
+
# @return [Integer, nil] matched byte length
|
|
77
|
+
#
|
|
78
|
+
def match_bytes(pattern) # rubocop:disable Naming/PredicateMethod -- returns byte length or nil, not a boolean
|
|
79
|
+
@scanner.match?(pattern)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Consumes n bytes from input and returns them as a pooled Slice.
|
|
83
|
+
# Companion to #match_bytes for atoms that measure their match in
|
|
84
|
+
# bytes (quantified regexes).
|
|
85
|
+
#
|
|
86
|
+
# @param count [Integer] number of bytes to consume
|
|
87
|
+
# @return [Parsanol::Slice] slice containing consumed bytes
|
|
88
|
+
#
|
|
89
|
+
# The underlying input string. Custom atoms doing position math
|
|
90
|
+
# (e.g. lookbehind rewinds) need direct access to it (GH-70).
|
|
91
|
+
#
|
|
92
|
+
|
|
93
|
+
# Rewinds n characters from the current position, walking back over
|
|
94
|
+
# UTF-8 continuation bytes so multibyte input stays aligned. Bounds:
|
|
95
|
+
# never rewinds past the start of the input.
|
|
96
|
+
#
|
|
97
|
+
# @param count [Integer] number of characters to move back
|
|
98
|
+
# @return [Integer] the new byte position
|
|
99
|
+
def rewind_chars(count)
|
|
100
|
+
pos = @scanner.pos
|
|
101
|
+
remaining = count
|
|
102
|
+
while remaining.positive? && pos.positive?
|
|
103
|
+
pos -= 1
|
|
104
|
+
remaining -= 1 unless (@raw_string.getbyte(pos) & 0xC0) == 0x80
|
|
105
|
+
end
|
|
106
|
+
@scanner.pos = pos
|
|
107
|
+
pos
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The underlying input string. Custom atoms doing position math
|
|
111
|
+
# (e.g. lookbehind rewinds) need direct access to it (GH-70).
|
|
112
|
+
#
|
|
113
|
+
# @return [String] the full input
|
|
114
|
+
def input
|
|
115
|
+
@raw_string
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def consume_bytes(count)
|
|
119
|
+
current_pos = @scanner.pos
|
|
120
|
+
content = @raw_string.byteslice(current_pos, count)
|
|
121
|
+
@scanner.pos = current_pos + count
|
|
122
|
+
@slice_pool.acquire_with(current_pos, content, @line_data)
|
|
123
|
+
end
|
|
124
|
+
|
|
72
125
|
# Consumes n characters from input and returns them as a pooled Slice.
|
|
73
126
|
#
|
|
74
127
|
# @param count [Integer] number of characters to consume
|
data/lib/parsanol/version.rb
CHANGED
data/lib/parsanol.rb
CHANGED
|
@@ -129,31 +129,10 @@ module Parsanol
|
|
|
129
129
|
def match(pattern = nil)
|
|
130
130
|
return CharacterClassBuilder.new unless pattern
|
|
131
131
|
|
|
132
|
-
match_quantifier_warning(pattern)
|
|
133
132
|
Atoms::Re.new(pattern)
|
|
134
133
|
end
|
|
135
134
|
module_function :match
|
|
136
135
|
|
|
137
|
-
# `match` consumes exactly one character (Parslet parity), so a regex
|
|
138
|
-
# quantifier is silently truncated: match(/[0-9]+/) matches a single
|
|
139
|
-
# digit. Warn once per offending pattern — the intended spelling is
|
|
140
|
-
# `match("[0-9]").repeat(1)`.
|
|
141
|
-
# Deliberately mutable: it accumulates warned patterns per process.
|
|
142
|
-
QUANTIFIER_WARNED = {} # rubocop:disable Style/MutableConstant
|
|
143
|
-
private_constant :QUANTIFIER_WARNED
|
|
144
|
-
|
|
145
|
-
def match_quantifier_warning(pattern)
|
|
146
|
-
return if QUANTIFIER_WARNED.key?(pattern)
|
|
147
|
-
return unless pattern.is_a?(String)
|
|
148
|
-
return unless quantified_pattern?(pattern)
|
|
149
|
-
|
|
150
|
-
QUANTIFIER_WARNED[pattern] = true
|
|
151
|
-
warn "Parsanol: match(#{pattern.inspect}) matches exactly one character " \
|
|
152
|
-
"(Parslet parity); the quantifier is ignored. " \
|
|
153
|
-
"Use match(...).repeat(1) to match repeatedly."
|
|
154
|
-
end
|
|
155
|
-
module_function :match_quantifier_warning
|
|
156
|
-
|
|
157
136
|
# True when the pattern carries a top-level quantifier (+, *, ?, or
|
|
158
137
|
# {n,m}) outside a character class.
|
|
159
138
|
def quantified_pattern?(pattern)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parsanol
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.37
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -89,6 +89,7 @@ files:
|
|
|
89
89
|
- lib/parsanol/native.rb
|
|
90
90
|
- lib/parsanol/native/batch_decoder.rb
|
|
91
91
|
- lib/parsanol/native/dynamic.rb
|
|
92
|
+
- lib/parsanol/native/event_player.rb
|
|
92
93
|
- lib/parsanol/native/ffi.rb
|
|
93
94
|
- lib/parsanol/native/parser.rb
|
|
94
95
|
- lib/parsanol/native/serializer.rb
|