parsanol 1.3.65-x86_64-linux → 1.3.67-x86_64-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/lutaml.rb +4 -0
- data/lib/parsanol/parg/parser.rb +43 -1
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +25 -18
- 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: 5a813b154280a5449a18ff5ef6d34e8832753535c5ef796e205879c43227e746
|
|
4
|
+
data.tar.gz: 8653688985a9a44ca4508991b87c3e5bd1f64ba1192b6cae6ee74f5fba6c236a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 804f2d7afab07c5ae7144fd22de2e69135df734673db91e76873754d8552f7c4086835b2209edf283011817aee709026524c1e0ddd6bd6ca0dd57c03dddab96d
|
|
7
|
+
data.tar.gz: db333c204ea7f3d4936b3672239ea12cf0d10fd146223ef7abbc81993dcb76689d44af314f88fb6188bea43fa29383d9607bb7d0f0648078f56f9b9fc8a7cb8d
|
|
@@ -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/lutaml.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Standalone requires of this file bypass the parg autoload registry —
|
|
4
|
+
# declare the namespace dependency so Artifact resolves.
|
|
5
|
+
require "parsanol/parg"
|
|
6
|
+
|
|
3
7
|
module Parsanol
|
|
4
8
|
module PARG
|
|
5
9
|
# lutaml-model integration: register a PARG artifact as a lutaml-model
|
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
data/lib/parsanol/vm.rb
CHANGED
|
@@ -175,8 +175,10 @@ module Parsanol
|
|
|
175
175
|
compiler.to_program
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
+
# Returns violation descriptions for this atom's ordered-choice
|
|
179
|
+
# branches (empty-matchable non-last branches shadow later ones).
|
|
178
180
|
def validate_choice_branches(atom)
|
|
179
|
-
atom.alternatives.each_with_index do |branch, idx|
|
|
181
|
+
atom.alternatives.each_with_index.filter_map do |branch, idx|
|
|
180
182
|
walk_branch = branch
|
|
181
183
|
next if idx == atom.alternatives.size - 1
|
|
182
184
|
|
|
@@ -194,14 +196,13 @@ module Parsanol
|
|
|
194
196
|
next if walk_branch.nil?
|
|
195
197
|
next unless Compiler.new.nullable?(walk_branch)
|
|
196
198
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"ones come last, or make the branch non-empty."
|
|
199
|
+
"alternative branch #{idx} (#{branch.inspect}) can match " \
|
|
200
|
+
"empty input and is not the last branch — ordered choice " \
|
|
201
|
+
"commits to its empty match, so " \
|
|
202
|
+
"#{atom.alternatives.size - idx - 1} later branch(es) can " \
|
|
203
|
+
"never match at positions where it matches empty. Reorder " \
|
|
204
|
+
"branches so empty-matchable ones come last, or make the " \
|
|
205
|
+
"branch non-empty."
|
|
205
206
|
end
|
|
206
207
|
end
|
|
207
208
|
|
|
@@ -211,6 +212,7 @@ module Parsanol
|
|
|
211
212
|
# the grammar is wrong, and inputs it would mis-parse fail with a
|
|
212
213
|
# baffling cause. Raise before any input is seen.
|
|
213
214
|
def validate_alternatives(root)
|
|
215
|
+
violations = []
|
|
214
216
|
visited = {}.compare_by_identity
|
|
215
217
|
walk = lambda do |atom, depth|
|
|
216
218
|
next if atom.nil? || depth > 20
|
|
@@ -219,26 +221,26 @@ module Parsanol
|
|
|
219
221
|
visited[atom] = true
|
|
220
222
|
case atom
|
|
221
223
|
when Parsanol::Atoms::Alternative
|
|
222
|
-
validate_choice_branches(atom)
|
|
224
|
+
violations.concat(validate_choice_branches(atom))
|
|
223
225
|
atom.alternatives.each { |c| walk.call(c, depth + 1) }
|
|
224
226
|
when Parsanol::Atoms::Sequence
|
|
225
227
|
atom.parslets.each { |c| walk.call(c, depth + 1) }
|
|
226
228
|
when Parsanol::Atoms::Repetition, Parsanol::Atoms::Named
|
|
227
229
|
walk.call(atom.parslet, depth + 1)
|
|
228
|
-
when Parsanol::Atoms::Lookahead
|
|
229
|
-
walk.call(atom.bound_parslet, depth + 1)
|
|
230
|
-
when Parsanol::Atoms::Ignored
|
|
231
|
-
walk.call(atom.wrapped_atom, depth + 1)
|
|
232
230
|
when Parsanol::Atoms::Entity
|
|
231
|
+
# Rule bodies hide behind the entity wrapper; without this
|
|
232
|
+
# descent the lint passes DSL grammars that fail the same
|
|
233
|
+
# grammar compiled from PARG (where bodies are inlined).
|
|
234
|
+
# Entities that cannot resolve outside a parse are skipped.
|
|
233
235
|
begin
|
|
234
236
|
walk.call(atom.parslet, depth + 1)
|
|
235
|
-
rescue Parsanol::GrammarError
|
|
236
|
-
raise
|
|
237
237
|
rescue StandardError
|
|
238
|
-
# Lazily resolved / recursive entity that cannot resolve
|
|
239
|
-
# outside a parse: skip this branch of the walk.
|
|
240
238
|
nil
|
|
241
239
|
end
|
|
240
|
+
when Parsanol::Atoms::Lookahead
|
|
241
|
+
walk.call(atom.bound_parslet, depth + 1)
|
|
242
|
+
when Parsanol::Atoms::Ignored
|
|
243
|
+
walk.call(atom.wrapped_atom, depth + 1)
|
|
242
244
|
when Parsanol::Atoms::Scope
|
|
243
245
|
begin
|
|
244
246
|
walk.call(atom.block.call, depth + 1)
|
|
@@ -248,6 +250,11 @@ module Parsanol
|
|
|
248
250
|
end
|
|
249
251
|
end
|
|
250
252
|
walk.call(root, 0)
|
|
253
|
+
return if violations.empty?
|
|
254
|
+
|
|
255
|
+
raise Parsanol::GrammarError,
|
|
256
|
+
"wrong grammar: #{violations.size} shadowed-alternative " \
|
|
257
|
+
"violation(s):\n - #{violations.join('\n - ')}"
|
|
251
258
|
end
|
|
252
259
|
|
|
253
260
|
# Executes a compiled program. Returns [true, value] on success;
|