parsanol 1.3.66-aarch64-linux → 1.3.67-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 +4 -4
- data/lib/parsanol/parg/compiler.rb +153 -1
- data/lib/parsanol/parg/document.rb +1 -1
- data/lib/parsanol/parg/parser.rb +43 -1
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ccfa3bf06978a6ae539be84940ca4bbff440b324644bf539960f979ca0237f5b
|
|
4
|
+
data.tar.gz: 79ab5492c25ac5b12f69c36a0761cd5894ba641fd6cae5322044d3965ad161e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33723ecbdfc86c933c24024e7b317c4d8d099a0759381f28587093847ad6bbe85249d7e8ec333f10914734735cc510bd77f0f1a2ad35bcbbae50b10d86737468
|
|
7
|
+
data.tar.gz: c17bf0042c56e63aeffb1a8efdcdc56f97888b00c0c4d22a8aeef9f18227315fa62cfc2b12c204e900f5d0e00ea88dd63711a550a6e78fe40599979eda0092a2
|
|
@@ -55,6 +55,17 @@ module Parsanol
|
|
|
55
55
|
@warnings = []
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# QoL shorthands (ABNF core rules): materialized on reference when
|
|
59
|
+
# the grammar does not define its own rule of that name.
|
|
60
|
+
BUILTIN_CLASSES = {
|
|
61
|
+
"DIGIT" => [[0x30, 0x39]],
|
|
62
|
+
"ALPHA" => [[0x41, 0x5A], [0x61, 0x7A]],
|
|
63
|
+
"ALNUM" => [[0x30, 0x39], [0x41, 0x5A], [0x61, 0x7A]],
|
|
64
|
+
"WORD" => [[0x30, 0x39], [0x41, 0x5A], [0x5F, 0x5F], [0x61, 0x7A]],
|
|
65
|
+
"HEXDIG" => [[0x30, 0x39], [0x41, 0x46], [0x61, 0x66]],
|
|
66
|
+
"ANY" => [[0x00, 0x10FFFF]],
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
58
69
|
def compile
|
|
59
70
|
@document.rules.each_key { |name| atom_for(name) }
|
|
60
71
|
errors = Lints.errors(@document, self)
|
|
@@ -79,7 +90,124 @@ module Parsanol
|
|
|
79
90
|
end
|
|
80
91
|
|
|
81
92
|
def atom_for(name)
|
|
82
|
-
@atom_cache[name] ||=
|
|
93
|
+
@atom_cache[name] ||= begin
|
|
94
|
+
atom = build_atom(fetch_rule(name))
|
|
95
|
+
if @document.skip_declaration && !skip_exempt_names.include?(name)
|
|
96
|
+
atom = inject_skip_trivia(atom)
|
|
97
|
+
# Entry points take leading and trailing skips: tail trivia
|
|
98
|
+
# (the tail-comment case) has no following terminal to
|
|
99
|
+
# trigger an injection, and full-consumption would reject
|
|
100
|
+
# it; leading trivia has no preceding interleave either.
|
|
101
|
+
if entry_rule?(name)
|
|
102
|
+
atom = Atoms::Sequence.new(skip_maybe_atom, atom, skip_maybe_atom)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
atom
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def entry_rule?(name)
|
|
110
|
+
@document.entries.value?(name) || @document.own_entries&.include?(name)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The skip declaration's rule references (and their transitive
|
|
114
|
+
# rule references) build without injection: the skip atom must
|
|
115
|
+
# not contain itself.
|
|
116
|
+
def skip_exempt_names
|
|
117
|
+
@skip_exempt_names ||= begin
|
|
118
|
+
names = Set.new
|
|
119
|
+
walk_refs = lambda do |node|
|
|
120
|
+
case node.kind
|
|
121
|
+
when :ref then names << node.a
|
|
122
|
+
when :alt, :seq then node.a.each { |b| walk_refs.call(b) }
|
|
123
|
+
when :cap then walk_refs.call(node.b)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
walk_refs.call(@document.skip_declaration) if @document.skip_declaration
|
|
127
|
+
closed = Set.new
|
|
128
|
+
until names.subset?(closed)
|
|
129
|
+
(names - closed).each do |ref|
|
|
130
|
+
closed << ref
|
|
131
|
+
walk_refs.call(@document.rules[ref]) if @document.rules.key?(ref)
|
|
132
|
+
end
|
|
133
|
+
names += closed
|
|
134
|
+
end
|
|
135
|
+
names
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def skip_wrapper?(atom)
|
|
140
|
+
@skip_wrapper_ids&.key?(atom)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def skip_maybe_atom
|
|
144
|
+
@skip_maybe_atom ||= begin
|
|
145
|
+
atom = build_atom(@document.skip_declaration)
|
|
146
|
+
while atom.is_a?(Atoms::Entity)
|
|
147
|
+
atom = atom.parslet
|
|
148
|
+
end
|
|
149
|
+
# the VM's internal compiler owns the ATOM-level nullability
|
|
150
|
+
# analysis (the node-level nullable? here works on rule nodes)
|
|
151
|
+
if Parsanol::VM::Compiler.new.nullable?(atom)
|
|
152
|
+
raise CompileError,
|
|
153
|
+
"skip rule must be non-nullable: a skip that can match " \
|
|
154
|
+
"empty loops forever at injection points"
|
|
155
|
+
end
|
|
156
|
+
# Ignored: consumed but contributes nothing to the enclosing
|
|
157
|
+
# sequence/repetition value — trivia never leaks into
|
|
158
|
+
# span-joined captures.
|
|
159
|
+
wrapper = Atoms::Ignored.new(Atoms::Repetition.new(atom, 0, 1))
|
|
160
|
+
(@skip_wrapper_ids ||= {}.compare_by_identity)[wrapper] = true
|
|
161
|
+
wrapper
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# parsanol-ruby#134: inject an optional match of the declared skip
|
|
166
|
+
# rule before every terminal of every rule. Injected skips are
|
|
167
|
+
# capture-free (a bare Repetition contributes no Named results),
|
|
168
|
+
# optional (ordered-choice/backtracking semantics preserved), and
|
|
169
|
+
# leave first sets unchanged (an optional prefix is epsilon-able).
|
|
170
|
+
def inject_skip_trivia(atom)
|
|
171
|
+
return atom if @document.skip_declaration.nil?
|
|
172
|
+
|
|
173
|
+
case atom
|
|
174
|
+
# rubocop:disable Lint/DuplicateBranch -- pass-through is the
|
|
175
|
+
# deliberate default; identical bodies, different decisions
|
|
176
|
+
when Atoms::Sequence
|
|
177
|
+
kids = atom.parslets.map { |child| inject_skip_trivia(child) }
|
|
178
|
+
out = []
|
|
179
|
+
kids.each do |child|
|
|
180
|
+
out << skip_maybe_atom unless skip_wrapper?(child)
|
|
181
|
+
out << child
|
|
182
|
+
end
|
|
183
|
+
out.length == 1 ? out.first : Atoms::Sequence.new(*out)
|
|
184
|
+
when Atoms::Alternative
|
|
185
|
+
Atoms::Alternative.new(*atom.alternatives.map { |a| inject_skip_trivia(a) })
|
|
186
|
+
when Atoms::Repetition
|
|
187
|
+
if skip_wrapper?(atom)
|
|
188
|
+
atom
|
|
189
|
+
else
|
|
190
|
+
Atoms::Repetition.new(inject_skip_trivia(atom.parslet), atom.min, atom.max,
|
|
191
|
+
atom.result_tag)
|
|
192
|
+
end
|
|
193
|
+
when Atoms::Named
|
|
194
|
+
# Pass-through: trivia must stay OUTSIDE captures. Injecting
|
|
195
|
+
# inside a Named makes the captured span include the skipped
|
|
196
|
+
# bytes (span-join swallows them into the captured value).
|
|
197
|
+
# Inter-token trivia around a capture is injected by the
|
|
198
|
+
# enclosing sequence's interleave.
|
|
199
|
+
atom
|
|
200
|
+
when Atoms::Lookahead
|
|
201
|
+
Atoms::Lookahead.new(inject_skip_trivia(atom.bound_parslet), atom.positive)
|
|
202
|
+
when Atoms::Str, Atoms::Re
|
|
203
|
+
# bare terminal as a whole rule body: leading trivia position
|
|
204
|
+
Atoms::Sequence.new(skip_maybe_atom, atom)
|
|
205
|
+
# pass-through is the deliberate default; identical bodies,
|
|
206
|
+
# different decisions
|
|
207
|
+
else
|
|
208
|
+
atom
|
|
209
|
+
end
|
|
210
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
83
211
|
end
|
|
84
212
|
|
|
85
213
|
def record_warning(message)
|
|
@@ -174,6 +302,10 @@ module Parsanol
|
|
|
174
302
|
if !@document.rules.key?(name) && @document.customs.key?(name)
|
|
175
303
|
return Node.new(:lit, "\u0001", false)
|
|
176
304
|
end
|
|
305
|
+
if !@document.rules.key?(name) && BUILTIN_CLASSES.key?(name)
|
|
306
|
+
# QoL shorthand stand-in: the analysis passes see the class
|
|
307
|
+
return Node.new(:class, BUILTIN_CLASSES[name])
|
|
308
|
+
end
|
|
177
309
|
|
|
178
310
|
@document.rules[name] || raise(CompileError, "unknown rule #{name.inspect}")
|
|
179
311
|
end
|
|
@@ -193,6 +325,15 @@ module Parsanol
|
|
|
193
325
|
Atoms::Str.new(node.a)
|
|
194
326
|
end
|
|
195
327
|
when :class then Atoms::Re.new(class_pattern(node.a))
|
|
328
|
+
when :until
|
|
329
|
+
# QoL: (*( !terminal any_char )) — consume everything up to
|
|
330
|
+
# (and excluding) the delimiter.
|
|
331
|
+
any_char = Atoms::Re.new(class_pattern([[0x00, 0x10FFFF]]))
|
|
332
|
+
Atoms::Repetition.new(
|
|
333
|
+
Atoms::Sequence.new(Atoms::Lookahead.new(Atoms::Str.new(node.a), false),
|
|
334
|
+
any_char),
|
|
335
|
+
0, nil
|
|
336
|
+
)
|
|
196
337
|
when :seq
|
|
197
338
|
atoms = node.a.map { |child| build_atom(child) }
|
|
198
339
|
atoms.length == 1 ? atoms.first : Atoms::Sequence.new(*atoms)
|
|
@@ -234,6 +375,10 @@ module Parsanol
|
|
|
234
375
|
if @document.customs.key?(node.a)
|
|
235
376
|
@dynamic = true
|
|
236
377
|
resolve_custom(node.a)
|
|
378
|
+
elsif !@document.rules.key?(node.a) && BUILTIN_CLASSES.key?(node.a)
|
|
379
|
+
# QoL: character-class shorthands (ABNF core rules) — usable
|
|
380
|
+
# unless the grammar defines its own rule of the same name.
|
|
381
|
+
Atoms::Re.new(class_pattern(BUILTIN_CLASSES[node.a]))
|
|
237
382
|
else
|
|
238
383
|
Atoms::Entity.new(node.a) { atom_for(node.a) }
|
|
239
384
|
end
|
|
@@ -472,6 +617,13 @@ module Parsanol
|
|
|
472
617
|
"docs" => @document.docs,
|
|
473
618
|
"source" => @document.source,
|
|
474
619
|
}
|
|
620
|
+
if @document.skip_declaration
|
|
621
|
+
# parsanol-ruby#134: declared trivia rule. Injection is baked
|
|
622
|
+
# into the compiled trees (plain atom kinds), so consumers
|
|
623
|
+
# without skip support still parse these artifacts correctly;
|
|
624
|
+
# the field records the grammar-level declaration.
|
|
625
|
+
envelope["skip"] = @document.skip_source
|
|
626
|
+
end
|
|
475
627
|
envelope["customs"] = @document.customs unless @document.customs.empty?
|
|
476
628
|
envelope["states"] = @document.states unless @document.states.empty?
|
|
477
629
|
envelope["dynamic"] = true if @dynamic
|
|
@@ -9,7 +9,7 @@ module Parsanol
|
|
|
9
9
|
|
|
10
10
|
Test = Struct.new(:entry, :kind, :input, :expect)
|
|
11
11
|
|
|
12
|
-
attr_accessor :grammar_name, :version, :source, :uses
|
|
12
|
+
attr_accessor :grammar_name, :version, :source, :uses, :skip_declaration, :skip_source
|
|
13
13
|
attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive,
|
|
14
14
|
:customs, :states
|
|
15
15
|
# Entries authored in this file (pre-import); nil-entry tests resolve
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -20,7 +20,8 @@ module Parsanol
|
|
|
20
20
|
|
|
21
21
|
STATE_KEYWORDS = %w[custom state].freeze
|
|
22
22
|
KEYWORDS = %w[grammar as alt from_table column bindings render derive
|
|
23
|
-
preprocess entry table_lookup custom state set switch
|
|
23
|
+
preprocess entry table_lookup custom state set switch
|
|
24
|
+
skip until].freeze
|
|
24
25
|
|
|
25
26
|
def initialize(text)
|
|
26
27
|
@tokens = Lexer.new(text).tokens
|
|
@@ -161,6 +162,19 @@ module Parsanol
|
|
|
161
162
|
skip_newlines
|
|
162
163
|
next
|
|
163
164
|
end
|
|
165
|
+
if peek&.type == :ident && peek.value == "skip"
|
|
166
|
+
advance
|
|
167
|
+
punct("=")
|
|
168
|
+
if document.skip_declaration
|
|
169
|
+
raise ParseError, "duplicate skip declaration"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
node = parse_choice
|
|
173
|
+
document.skip_declaration = validate_skip_node(node)
|
|
174
|
+
document.skip_source = skip_source_string(node)
|
|
175
|
+
skip_newlines
|
|
176
|
+
next
|
|
177
|
+
end
|
|
164
178
|
if peek&.type == :ident && STATE_KEYWORDS.include?(peek.value)
|
|
165
179
|
keyword = advance.value
|
|
166
180
|
slot = ident.value
|
|
@@ -188,6 +202,30 @@ module Parsanol
|
|
|
188
202
|
punct("}")
|
|
189
203
|
end
|
|
190
204
|
|
|
205
|
+
# The skip declaration may only name rules (a rule reference or an
|
|
206
|
+
# alternation of rule references) — arbitrary expressions would
|
|
207
|
+
# make the injected trivia unanalyzable.
|
|
208
|
+
def skip_source_string(node)
|
|
209
|
+
case node.kind
|
|
210
|
+
when :ref then node.a
|
|
211
|
+
when :alt then node.a.map { |b| skip_source_string(b) }.join(" / ")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def validate_skip_node(node)
|
|
216
|
+
case node.kind
|
|
217
|
+
when :ref
|
|
218
|
+
node
|
|
219
|
+
when :alt
|
|
220
|
+
node.a.each { |branch| validate_skip_node(branch) }
|
|
221
|
+
node
|
|
222
|
+
else
|
|
223
|
+
raise ParseError,
|
|
224
|
+
"skip must be a rule reference or an alternation of rule " \
|
|
225
|
+
"references, got #{node.kind.inspect}"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
191
229
|
def rule_name
|
|
192
230
|
token = ident
|
|
193
231
|
if KEYWORDS.include?(token.value)
|
|
@@ -325,6 +363,10 @@ module Parsanol
|
|
|
325
363
|
end
|
|
326
364
|
punct("}")
|
|
327
365
|
Node.new(:switch, slot, arms)
|
|
366
|
+
when "until"
|
|
367
|
+
# QoL: any-until terminal — (*( !terminal any_char )), the
|
|
368
|
+
# idiomatic "consume everything up to the delimiter" run.
|
|
369
|
+
Node.new(:until, unquote(expect(:str).value))
|
|
328
370
|
when "alt"
|
|
329
371
|
ident("from_table")
|
|
330
372
|
table = unquote(expect(:str).value)
|
data/lib/parsanol/version.rb
CHANGED